valetudo-map-parser 0.1.9b3__tar.gz → 0.1.9b5__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 (21) hide show
  1. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/PKG-INFO +1 -1
  2. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/config/colors.py +2 -2
  3. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/config/rand25_parser.py +7 -2
  4. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/config/shared.py +3 -3
  5. valetudo_map_parser-0.1.9b5/SCR/valetudo_map_parser/config/utils.py +132 -0
  6. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/hypfer_draw.py +1 -27
  7. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/hypfer_handler.py +25 -130
  8. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/images_utils.py +2 -65
  9. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/map_data.py +1 -6
  10. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/rand25_handler.py +21 -99
  11. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/reimg_draw.py +2 -22
  12. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/pyproject.toml +1 -1
  13. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/LICENSE +0 -0
  14. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/NOTICE.txt +0 -0
  15. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/README.md +0 -0
  16. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/__init__.py +8 -8
  17. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  18. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
  19. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/config/drawable.py +1 -1
  20. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/SCR/valetudo_map_parser/config/types.py +2 -2
  21. {valetudo_map_parser-0.1.9b3 → valetudo_map_parser-0.1.9b5}/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.9b3
3
+ Version: 0.1.9b5
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
@@ -2,9 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from enum import StrEnum
6
- from typing import List, Dict, Tuple
7
5
  import logging
6
+ from enum import StrEnum
7
+ from typing import Dict, List, Tuple
8
8
 
9
9
  _LOGGER = logging.getLogger(__name__)
10
10
 
@@ -5,10 +5,10 @@ Version: v2024.08.2
5
5
  - Additional functions are to get in our image_handler the images datas.
6
6
  """
7
7
 
8
- from enum import Enum
9
8
  import math
10
9
  import struct
11
- from typing import Dict, List, Optional, Callable, TypeVar, Any
10
+ from enum import Enum
11
+ from typing import Any, Callable, Dict, List, Optional, TypeVar
12
12
 
13
13
  _CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
14
14
 
@@ -387,6 +387,11 @@ class RRMapParser:
387
387
  for zone in blocks[RRMapParser.Types.FORBIDDEN_MOP_ZONES.value]
388
388
  ]
389
389
 
390
+ if RRMapParser.Types.GOTO_TARGET.value in blocks:
391
+ parsed_map_data["goto_target"] = blocks[
392
+ RRMapParser.Types.GOTO_TARGET.value
393
+ ]["position"]
394
+
390
395
  def parse_data(
391
396
  self, payload: Optional[bytes] = None, pixels: bool = False
392
397
  ) -> Optional[Dict[str, Any]]:
@@ -9,7 +9,9 @@ import logging
9
9
 
10
10
  from .types import (
11
11
  ATTR_CALIBRATION_POINTS,
12
+ ATTR_CAMERA_MODE,
12
13
  ATTR_MARGINS,
14
+ ATTR_OBSTACLES,
13
15
  ATTR_POINTS,
14
16
  ATTR_ROOMS,
15
17
  ATTR_ROTATE,
@@ -19,8 +21,6 @@ from .types import (
19
21
  ATTR_VACUUM_POSITION,
20
22
  ATTR_VACUUM_STATUS,
21
23
  ATTR_ZONES,
22
- ATTR_CAMERA_MODE,
23
- ATTR_OBSTACLES,
24
24
  CONF_ASPECT_RATIO,
25
25
  CONF_AUTO_ZOOM,
26
26
  CONF_OFFSET_BOTTOM,
@@ -35,8 +35,8 @@ from .types import (
35
35
  CONF_ZOOM_LOCK_RATIO,
36
36
  DEFAULT_VALUES,
37
37
  CameraModes,
38
+ Colors,
38
39
  )
39
- from .types import Colors
40
40
 
41
41
  _LOGGER = logging.getLogger(__name__)
42
42
 
@@ -0,0 +1,132 @@
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
+ ]
@@ -6,16 +6,9 @@ Version: 2024.07.2
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import hashlib
10
- import json
11
9
  import logging
12
10
 
13
- from .config.types import (
14
- Color,
15
- JsonType,
16
- NumpyArray,
17
- RobotPosition,
18
- )
11
+ from .config.types import Color, JsonType, NumpyArray, RobotPosition
19
12
 
20
13
  _LOGGER = logging.getLogger(__name__)
21
14
 
@@ -288,25 +281,6 @@ class ImageDraw:
288
281
  _LOGGER.info("%s: Got the points in the json.", self.file_name)
289
282
  return entity_dict
290
283
 
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
284
  async def async_get_robot_in_room(
311
285
  self, robot_y: int = 0, robot_x: int = 0, angle: float = 0.0
312
286
  ) -> RobotPosition:
@@ -12,58 +12,35 @@ import logging
12
12
 
13
13
  from PIL import Image
14
14
 
15
- from .config.types import (
16
- CalibrationPoints,
17
- ChargerPosition,
18
- ImageSize,
19
- RobotPosition,
20
- RoomsProperties,
21
- )
22
15
  from .config.auto_crop import AutoCrop
23
16
  from .config.drawable import Drawable
24
17
  from .config.shared import CameraShared
18
+ from .config.types import COLORS, CalibrationPoints, Colors, RoomsProperties
19
+ from .config.utils import BaseHandler
20
+ from .hypfer_draw import ImageDraw as ImDraw
25
21
  from .map_data import ImageData
26
- from .images_utils import (
27
- ImageUtils as ImUtils,
28
- resize_to_aspect_ratio,
29
- )
30
- from .hypfer_draw import (
31
- ImageDraw as ImDraw,
32
- )
33
- from .config.colors import ColorsManagment, SupportedColor
34
22
 
35
23
  _LOGGER = logging.getLogger(__name__)
36
24
 
37
25
 
38
- class HypferMapImageHandler:
26
+ class HypferMapImageHandler(BaseHandler):
39
27
  """Map Image Handler Class.
40
28
  This class is used to handle the image data and the drawing of the map."""
41
29
 
42
30
  def __init__(self, shared_data: CameraShared):
43
31
  """Initialize the Map Image Handler."""
32
+ super().__init__()
44
33
  self.shared = shared_data # camera shared data
45
- self.file_name = self.shared.file_name # file name of the vacuum.
46
34
  self.auto_crop = None # auto crop data to be calculate once.
47
35
  self.calibration_data = None # camera shared data.
48
- self.charger_pos = None # vacuum data charger position.
49
36
  self.crop_area = None # module shared for calibration data.
50
- self.crop_img_size = None # size of the image cropped calibration data.
51
37
  self.data = ImageData # imported Image Data Module.
52
38
  self.draw = Drawable # imported Drawing utilities
53
39
  self.go_to = None # vacuum go to data
54
40
  self.img_hash = None # hash of the image calculated to check differences.
55
41
  self.img_base_layer = None # numpy array store the map base layer.
56
- self.img_size = None # size of the created image
57
- self.json_data = None # local stored and shared json data.
58
- self.json_id = None # grabbed data of the vacuum image id.
59
- self.path_pixels = None # vacuum path datas.
60
- self.robot_in_room = None # vacuum room position.
61
- self.robot_pos = None # vacuum coordinates.
62
- self.room_propriety = None # vacuum segments data.
63
- self.rooms_pos = None # vacuum room coordinates / name list.
64
42
  self.active_zones = None # vacuum active zones.
65
43
  self.frame_number = 0 # frame number of the image.
66
- self.max_frames = 1024
67
44
  self.zooming = False # zooming the image.
68
45
  self.svg_wait = False # SVG image creation wait.
69
46
  self.trim_down = 0 # memory stored trims calculated once.
@@ -77,11 +54,9 @@ class HypferMapImageHandler:
77
54
  self.offset_x = 0 # offset x for the aspect ratio.
78
55
  self.offset_y = 0 # offset y for the aspect ratio.
79
56
  self.imd = ImDraw(self)
80
- self.imu = ImUtils(self)
81
57
  self.ac = AutoCrop(self)
82
- self.colors_manager = ColorsManagment({})
83
- self.rooms_colors = self.colors_manager.get_rooms_colors()
84
58
  self.color_grey = (128, 128, 128, 255)
59
+ self.file_name = self.shared.file_name # file name of the vacuum.
85
60
 
86
61
  async def async_extract_room_properties(self, json_data) -> RoomsProperties:
87
62
  """Extract room properties from the JSON data."""
@@ -132,23 +107,6 @@ class HypferMapImageHandler:
132
107
  self.rooms_pos = None
133
108
  return room_properties
134
109
 
135
- async def _async_initialize_colors(self):
136
- """Initialize and return all required colors."""
137
- return {
138
- "color_wall": self.colors_manager.get_colour(SupportedColor.WALLS),
139
- "color_no_go": self.colors_manager.get_colour(SupportedColor.NO_GO),
140
- "color_go_to": self.colors_manager.get_colour(SupportedColor.GO_TO),
141
- "color_robot": self.colors_manager.get_colour(SupportedColor.ROBOT),
142
- "color_charger": self.colors_manager.get_colour(SupportedColor.CHARGER),
143
- "color_move": self.colors_manager.get_colour(SupportedColor.PATH),
144
- "color_background": self.colors_manager.get_colour(
145
- SupportedColor.MAP_BACKGROUND
146
- ),
147
- "color_zone_clean": self.colors_manager.get_colour(
148
- SupportedColor.ZONE_CLEAN
149
- ),
150
- }
151
-
152
110
  # noinspection PyUnresolvedReferences,PyUnboundLocalVariable
153
111
  async def async_get_image_from_json(
154
112
  self,
@@ -161,7 +119,9 @@ class HypferMapImageHandler:
161
119
  @return Image.Image: The image to display.
162
120
  """
163
121
  # Initialize the colors.
164
- colors = await self._async_initialize_colors()
122
+ colors: Colors = {
123
+ name: self.shared.user_colors[idx] for idx, name in enumerate(COLORS)
124
+ }
165
125
  # Check if the JSON data is not None else process the image.
166
126
  try:
167
127
  if m_json is not None:
@@ -190,12 +150,12 @@ class HypferMapImageHandler:
190
150
  # Get the pixels size and layers from the JSON data
191
151
  pixel_size = int(m_json["pixelSize"])
192
152
  layers, active = self.data.find_layers(m_json["layers"], {}, [])
193
- new_frame_hash = await self.imd.calculate_array_hash(layers, active)
153
+ new_frame_hash = await self.calculate_array_hash(layers, active)
194
154
  if self.frame_number == 0:
195
155
  self.img_hash = new_frame_hash
196
156
  # empty image
197
157
  img_np_array = await self.draw.create_empty_image(
198
- size_x, size_y, colors["color_background"]
158
+ size_x, size_y, colors["background"]
199
159
  )
200
160
  # overlapping layers and segments
201
161
  for layer_type, compressed_pixels_list in layers.items():
@@ -203,21 +163,21 @@ class HypferMapImageHandler:
203
163
  img_np_array,
204
164
  compressed_pixels_list,
205
165
  layer_type,
206
- colors["color_wall"],
207
- colors["color_zone_clean"],
166
+ colors["wall"],
167
+ colors["zone_clean"],
208
168
  pixel_size,
209
169
  )
210
170
  # Draw the virtual walls if any.
211
171
  img_np_array = await self.imd.async_draw_virtual_walls(
212
- m_json, img_np_array, colors["color_no_go"]
172
+ m_json, img_np_array, colors["no_go"]
213
173
  )
214
174
  # Draw charger.
215
175
  img_np_array = await self.imd.async_draw_charger(
216
- img_np_array, entity_dict, colors["color_charger"]
176
+ img_np_array, entity_dict, colors["charger"]
217
177
  )
218
178
  # Draw obstacles if any.
219
179
  img_np_array = await self.imd.async_draw_obstacle(
220
- img_np_array, entity_dict, colors["color_no_go"]
180
+ img_np_array, entity_dict, colors["no_go"]
221
181
  )
222
182
  # Robot and rooms position
223
183
  if (room_id > 0) and not self.room_propriety:
@@ -232,7 +192,7 @@ class HypferMapImageHandler:
232
192
  )
233
193
  _LOGGER.info("%s: Completed base Layers", self.file_name)
234
194
  # Copy the new array in base layer.
235
- self.img_base_layer = await self.imd.async_copy_array(img_np_array)
195
+ self.img_base_layer = await self.async_copy_array(img_np_array)
236
196
  self.shared.frame_number = self.frame_number
237
197
  self.frame_number += 1
238
198
  if (self.frame_number >= self.max_frames) or (
@@ -246,18 +206,18 @@ class HypferMapImageHandler:
246
206
  str(self.frame_number),
247
207
  )
248
208
  # Copy the base layer to the new image.
249
- img_np_array = await self.imd.async_copy_array(self.img_base_layer)
209
+ img_np_array = await self.async_copy_array(self.img_base_layer)
250
210
  # All below will be drawn at each frame.
251
211
  # Draw zones if any.
252
- img_np_array = await self.imd.async_draw_zones(
212
+ img_np_array = await self.async_draw_zones(
253
213
  m_json,
254
214
  img_np_array,
255
- colors["color_zone_clean"],
256
- colors["color_no_go"],
215
+ colors["zone_clean"],
216
+ colors["no_go"],
257
217
  )
258
218
  # Draw the go_to target flag.
259
219
  img_np_array = await self.imd.draw_go_to_flag(
260
- img_np_array, entity_dict, colors["color_go_to"]
220
+ img_np_array, entity_dict, colors["go_to"]
261
221
  )
262
222
  # Draw path prediction and paths.
263
223
  img_np_array = await self.imd.async_draw_paths(
@@ -304,15 +264,8 @@ class HypferMapImageHandler:
304
264
  ):
305
265
  width = self.shared.image_ref_width
306
266
  height = self.shared.image_ref_height
307
- (
308
- resized_image,
309
- self.crop_img_size,
310
- ) = await resize_to_aspect_ratio(
311
- pil_img,
312
- width,
313
- height,
314
- self.shared.image_aspect_ratio,
315
- self.async_map_coordinates_offset,
267
+ resized_image = await self.async_resize_image(
268
+ pil_img, width, height, self.shared.image_aspect_ratio
316
269
  )
317
270
  return resized_image
318
271
  _LOGGER.debug("%s: Frame Completed.", self.file_name)
@@ -326,26 +279,6 @@ class HypferMapImageHandler:
326
279
  )
327
280
  return None
328
281
 
329
- def get_frame_number(self) -> int:
330
- """Return the frame number of the image."""
331
- return self.frame_number
332
-
333
- def get_robot_position(self) -> RobotPosition | None:
334
- """Return the robot position."""
335
- return self.robot_pos
336
-
337
- def get_charger_position(self) -> ChargerPosition | None:
338
- """Return the charger position."""
339
- return self.charger_pos
340
-
341
- def get_img_size(self) -> ImageSize | None:
342
- """Return the size of the image."""
343
- return self.img_size
344
-
345
- def get_json_id(self) -> str | None:
346
- """Return the JSON ID from the image."""
347
- return self.json_id
348
-
349
282
  async def async_get_rooms_attributes(self) -> RoomsProperties:
350
283
  """Get the rooms attributes from the JSON data.
351
284
  :return: The rooms attribute's."""
@@ -368,15 +301,7 @@ class HypferMapImageHandler:
368
301
  _LOGGER.info("Getting %s Calibrations points.", self.file_name)
369
302
 
370
303
  # Define the map points (fixed)
371
- map_points = [
372
- {"x": 0, "y": 0}, # Top-left corner 0
373
- {"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
374
- {
375
- "x": self.crop_img_size[0],
376
- "y": self.crop_img_size[1],
377
- }, # Bottom-right corner 2
378
- {"x": 0, "y": self.crop_img_size[1]}, # Bottom-left corner (optional) 3
379
- ]
304
+ map_points = self.get_map_points()
380
305
  # Calculate the calibration points in the vacuum coordinate system
381
306
  vacuum_points = self.imu.get_vacuum_points(rotation_angle)
382
307
 
@@ -386,33 +311,3 @@ class HypferMapImageHandler:
386
311
  calibration_data.append(calibration_point)
387
312
  del vacuum_points, map_points, calibration_point, rotation_angle # free memory.
388
313
  return calibration_data
389
-
390
- async def async_map_coordinates_offset(
391
- self, wsf: int, hsf: int, width: int, height: int
392
- ) -> tuple[int, int]:
393
- """
394
- Offset the coordinates to the map.
395
- :param wsf: Width scale factor.
396
- :param hsf: Height scale factor.
397
- :param width: Width of the image.
398
- :param height: Height of the image.
399
- :return: A tuple containing the adjusted (width, height) values
400
- :raises ValueError: If any input parameters are negative
401
- """
402
-
403
- if any(x < 0 for x in (wsf, hsf, width, height)):
404
- raise ValueError("All parameters must be positive integers")
405
-
406
- if wsf == 1 and hsf == 1:
407
- self.imu.set_image_offset_ratio_1_1(width, height)
408
- elif wsf == 2 and hsf == 1:
409
- self.imu.set_image_offset_ratio_2_1(width, height)
410
- elif wsf == 3 and hsf == 2:
411
- self.imu.set_image_offset_ratio_3_2(width, height)
412
- elif wsf == 5 and hsf == 4:
413
- self.imu.set_image_offset_ratio_5_4(width, height)
414
- elif wsf == 9 and hsf == 16:
415
- self.imu.set_image_offset_ratio_9_16(width, height)
416
- elif wsf == 16 and hsf == 9:
417
- self.imu.set_image_offset_ratio_16_9(width, height)
418
- return width, height
@@ -8,8 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import logging
10
10
 
11
- from PIL import Image, ImageOps
12
-
13
11
  _LOGGER = logging.getLogger(__name__)
14
12
 
15
13
 
@@ -295,7 +293,7 @@ class ImageUtils:
295
293
  )
296
294
 
297
295
  async def async_zone_propriety(self, zones_data) -> dict:
298
- """Get the zone propiety"""
296
+ """Get the zone propriety"""
299
297
  zone_properties = {}
300
298
  id_count = 1
301
299
  for zone in zones_data:
@@ -316,7 +314,7 @@ class ImageUtils:
316
314
  return zone_properties
317
315
 
318
316
  async def async_points_propriety(self, points_data) -> dict:
319
- """Get the point propiety"""
317
+ """Get the point propriety"""
320
318
  point_properties = {}
321
319
  id_count = 1
322
320
  for point in points_data:
@@ -335,64 +333,3 @@ class ImageUtils:
335
333
  if id_count > 1:
336
334
  _LOGGER.debug("%s: Point Properties updated.", self.file_name)
337
335
  return point_properties
338
-
339
-
340
- async def resize_to_aspect_ratio(
341
- pil_img: Image.Image,
342
- ref_width: int,
343
- ref_height: int,
344
- aspect_ratio: str = "None",
345
- async_map_coordinates_offset=None,
346
- ) -> tuple:
347
- """
348
- Resize the image to match the given aspect ratio, maintaining the camera's aspect ratio.
349
-
350
- Args:
351
- pil_img (PIL.Image): The input image to resize.
352
- ref_width (int): The reference width for the image.
353
- ref_height (int): The reference height for the image.
354
- aspect_ratio (str): Aspect ratio in the format "width,height" or "None" for default.
355
- async_map_coordinates_offset (callable): Async function to compute coordinate offsets.
356
-
357
- Returns:
358
- tuple: A resized image and crop image size as a tuple (PIL.Image, list).
359
- """
360
- crop_img_size = [0, 0]
361
-
362
- if aspect_ratio and aspect_ratio != "None":
363
- try:
364
- # Parse aspect ratio (e.g., "16,9")
365
- wsf, hsf = [int(x) for x in aspect_ratio.split(",")]
366
- new_aspect_ratio = wsf / hsf
367
-
368
- # Calculate current aspect ratio
369
- current_aspect_ratio = ref_width / ref_height
370
-
371
- # Resize based on aspect ratio comparison
372
- if current_aspect_ratio > new_aspect_ratio:
373
- new_width = int(pil_img.height * new_aspect_ratio)
374
- new_height = pil_img.height
375
- else:
376
- new_width = pil_img.width
377
- new_height = int(pil_img.width / new_aspect_ratio)
378
-
379
- # Resize image using padding
380
- resized_img = ImageOps.pad(pil_img, (new_width, new_height))
381
-
382
- # Compute crop image size if mapping offset function is provided
383
- if async_map_coordinates_offset:
384
- (
385
- crop_img_size[0],
386
- crop_img_size[1],
387
- ) = await async_map_coordinates_offset(wsf, hsf, new_width, new_height)
388
-
389
- return resized_img, crop_img_size
390
-
391
- except Exception as e:
392
- _LOGGER.debug(
393
- "Error resizing image with aspect ratio: %s. %s", aspect_ratio, e
394
- )
395
- raise ValueError("Error resizing image with aspect ratio") from e
396
-
397
- # If no aspect ratio is provided, return the original image and default crop size
398
- return pil_img, crop_img_size
@@ -10,12 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import numpy as np
12
12
 
13
- from .config.types import (
14
- Colors,
15
- ImageSize,
16
- JsonType,
17
- NumpyArray,
18
- )
13
+ from .config.types import Colors, ImageSize, JsonType, NumpyArray
19
14
 
20
15
 
21
16
  class ImageData:
@@ -9,13 +9,22 @@ from __future__ import annotations
9
9
 
10
10
  import logging
11
11
  import uuid
12
-
13
- from PIL import Image, ImageOps
14
12
  from typing import Any
15
- from .config.types import COLORS, DEFAULT_IMAGE_SIZE, DEFAULT_PIXEL_SIZE
16
- from .config.types import Colors, JsonType, PilPNG, RobotPosition, RoomsProperties
13
+
14
+ from PIL import Image
15
+
17
16
  from .config.auto_crop import AutoCrop
18
- from .images_utils import ImageUtils as ImUtils
17
+ from .config.types import (
18
+ COLORS,
19
+ DEFAULT_IMAGE_SIZE,
20
+ DEFAULT_PIXEL_SIZE,
21
+ Colors,
22
+ JsonType,
23
+ PilPNG,
24
+ RobotPosition,
25
+ RoomsProperties,
26
+ )
27
+ from .config.utils import BaseHandler
19
28
  from .map_data import RandImageData
20
29
  from .reimg_draw import ImageDraw
21
30
 
@@ -23,33 +32,23 @@ _LOGGER = logging.getLogger(__name__)
23
32
 
24
33
 
25
34
  # noinspection PyTypeChecker
26
- class ReImageHandler:
35
+ class ReImageHandler(BaseHandler):
27
36
  """
28
37
  Image Handler for Valetudo Re Vacuums.
29
38
  """
30
39
 
31
40
  def __init__(self, camera_shared):
41
+ super().__init__()
32
42
  self.auto_crop = None # Auto crop flag
33
43
  self.segment_data = None # Segment data
34
44
  self.outlines = None # Outlines data
35
45
  self.calibration_data = None # Calibration data
36
- self.charger_pos = None # Charger position
37
46
  self.crop_area = None # Crop area
38
- self.crop_img_size = [] # Crop image size
39
47
  self.data = RandImageData # Image Data
40
- self.frame_number = 0 # Image Frame number
41
- self.max_frames = 1024
42
48
  self.go_to = None # Go to position data
43
49
  self.img_base_layer = None # Base image layer
44
50
  self.img_rotate = camera_shared.image_rotate # Image rotation
45
- self.img_size = None # Image size
46
- self.json_data = None # Json data
47
- self.json_id = None # Json id
48
- self.path_pixels = None # Path pixels data
49
- self.robot_in_room = None # Robot in room data
50
- self.robot_pos = None # Robot position
51
51
  self.room_propriety = None # Room propriety data
52
- self.rooms_pos = None # Rooms position data
53
52
  self.shared = camera_shared # Shared data
54
53
  self.active_zones = None # Active zones
55
54
  self.trim_down = None # Trim down
@@ -65,7 +64,6 @@ class ReImageHandler:
65
64
  self.offset_left = self.shared.offset_left # offset left
66
65
  self.offset_right = self.shared.offset_right # offset right
67
66
  self.imd = ImageDraw(self) # Image Draw
68
- self.imu = ImUtils(self) # Image Utils
69
67
  self.ac = AutoCrop(self)
70
68
 
71
69
  async def extract_room_properties(
@@ -177,7 +175,7 @@ class ReImageHandler:
177
175
 
178
176
  # Increment frame number
179
177
  self.frame_number += 1
180
- img_np_array = await self.imd.async_copy_array(self.img_base_layer)
178
+ img_np_array = await self.async_copy_array(self.img_base_layer)
181
179
  _LOGGER.debug(
182
180
  "%s: Frame number %s", self.file_name, str(self.frame_number)
183
181
  )
@@ -231,7 +229,7 @@ class ReImageHandler:
231
229
  (robot_position[1] * 10),
232
230
  robot_position_angle,
233
231
  )
234
- self.img_base_layer = await self.imd.async_copy_array(img_np_array)
232
+ self.img_base_layer = await self.async_copy_array(img_np_array)
235
233
  return self.img_base_layer, robot_position, robot_position_angle
236
234
 
237
235
  async def _draw_map_elements(
@@ -276,59 +274,12 @@ class ReImageHandler:
276
274
  width = self.shared.image_ref_width
277
275
  height = self.shared.image_ref_height
278
276
  if self.shared.image_aspect_ratio != "None":
279
- wsf, hsf = [int(x) for x in self.shared.image_aspect_ratio.split(",")]
280
- _LOGGER.debug("Aspect Ratio: %s, %s", str(wsf), str(hsf))
281
- if wsf == 0 or hsf == 0:
282
- return pil_img
283
- new_aspect_ratio = wsf / hsf
284
- aspect_ratio = width / height
285
- if aspect_ratio > new_aspect_ratio:
286
- new_width = int(pil_img.height * new_aspect_ratio)
287
- new_height = pil_img.height
288
- else:
289
- new_width = pil_img.width
290
- new_height = int(pil_img.width / new_aspect_ratio)
291
-
292
- resized = ImageOps.pad(pil_img, (new_width, new_height))
293
- (
294
- self.crop_img_size[0],
295
- self.crop_img_size[1],
296
- ) = await self.async_map_coordinates_offset(
297
- wsf, hsf, new_width, new_height
298
- )
299
- _LOGGER.debug(
300
- "%s: Image Aspect Ratio: %s, %s",
301
- self.file_name,
302
- str(wsf),
303
- str(hsf),
277
+ pil_img = await self.async_resize_image(
278
+ pil_img, width, height, self.shared.image_aspect_ratio, True
304
279
  )
305
- _LOGGER.debug("%s: Resized Frame Completed.", self.file_name)
306
- return resized
307
- _LOGGER.debug("%s: Padded Frame Completed.", self.file_name)
308
- return ImageOps.pad(pil_img, (width, height))
309
280
  _LOGGER.debug("%s: Frame Completed.", self.file_name)
310
281
  return pil_img
311
282
 
312
- def get_frame_number(self) -> int:
313
- """Return the frame number."""
314
- return self.frame_number
315
-
316
- def get_robot_position(self) -> Any:
317
- """Return the robot position."""
318
- return self.robot_pos
319
-
320
- def get_charger_position(self) -> Any:
321
- """Return the charger position."""
322
- return self.charger_pos
323
-
324
- def get_img_size(self) -> Any:
325
- """Return the image size."""
326
- return self.img_size
327
-
328
- def get_json_id(self) -> str:
329
- """Return the json id."""
330
- return self.json_id
331
-
332
283
  async def get_rooms_attributes(
333
284
  self, destinations: JsonType = None
334
285
  ) -> RoomsProperties:
@@ -427,15 +378,7 @@ class ReImageHandler:
427
378
  )
428
379
 
429
380
  # Define the map points (fixed)
430
- map_points = [
431
- {"x": 0, "y": 0}, # Top-left corner 0
432
- {"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
433
- {
434
- "x": self.crop_img_size[0],
435
- "y": self.crop_img_size[1],
436
- }, # Bottom-right corner 2
437
- {"x": 0, "y": self.crop_img_size[1]}, # Bottom-left corner (optional) 3
438
- ]
381
+ map_points = self.get_map_points()
439
382
 
440
383
  # Valetudo Re version need corrections of the coordinates and are implemented with *10
441
384
  vacuum_points = self.imu.re_get_vacuum_points(rotation_angle)
@@ -446,24 +389,3 @@ class ReImageHandler:
446
389
  self.calibration_data.append(calibration_point)
447
390
 
448
391
  return self.calibration_data
449
-
450
- async def async_map_coordinates_offset(
451
- self, wsf: int, hsf: int, width: int, height: int
452
- ) -> tuple[int, int]:
453
- """
454
- Offset the coordinates to the map.
455
- """
456
-
457
- if wsf == 1 and hsf == 1:
458
- self.imu.set_image_offset_ratio_1_1(width, height, rand256=True)
459
- elif wsf == 2 and hsf == 1:
460
- self.imu.set_image_offset_ratio_2_1(width, height, rand256=True)
461
- elif wsf == 3 and hsf == 2:
462
- self.imu.set_image_offset_ratio_3_2(width, height, rand256=True)
463
- elif wsf == 5 and hsf == 4:
464
- self.imu.set_image_offset_ratio_5_4(width, height, rand256=True)
465
- elif wsf == 9 and hsf == 16:
466
- self.imu.set_image_offset_ratio_9_16(width, height, rand256=True)
467
- elif wsf == 16 and hsf == 9:
468
- self.imu.set_image_offset_ratio_16_9(width, height, rand256=True)
469
- return width, height
@@ -10,10 +10,9 @@ import hashlib
10
10
  import json
11
11
  import logging
12
12
 
13
- from .config.types import Color, JsonType, NumpyArray
14
13
  from .config.drawable import Drawable
15
- from .map_data import RandImageData
16
- from .map_data import ImageData
14
+ from .config.types import Color, JsonType, NumpyArray
15
+ from .map_data import ImageData, RandImageData
17
16
 
18
17
  _LOGGER = logging.getLogger(__name__)
19
18
 
@@ -300,25 +299,6 @@ class ImageDraw:
300
299
  _LOGGER.info("%s: Got the points in the json.", self.file_name)
301
300
  return entity_dict
302
301
 
303
- @staticmethod
304
- async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
305
- """Copy the array."""
306
- return NumpyArray.copy(original_array)
307
-
308
- async def calculate_array_hash(self, layers: dict, active: list[int] = None) -> str:
309
- """Calculate the hash of the image based on the layers and active segments walls."""
310
- self.img_h.active_zones = active
311
- if layers and active:
312
- data_to_hash = {
313
- "layers": len(layers["wall"][0]),
314
- "active_segments": tuple(active),
315
- }
316
- data_json = json.dumps(data_to_hash, sort_keys=True)
317
- hash_value = hashlib.sha256(data_json.encode()).hexdigest()
318
- else:
319
- hash_value = None
320
- return hash_value
321
-
322
302
  async def async_get_robot_position(self, m_json: JsonType) -> tuple | None:
323
303
  """Get the robot position from the entity data."""
324
304
  robot_pos = None
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "valetudo-map-parser"
3
- version = "0.1.9.b3"
3
+ version = "0.1.9.b5"
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,20 +1,20 @@
1
1
  """Valetudo map parser.
2
2
  Version: 0.1.8"""
3
3
 
4
- from .hypfer_handler import HypferMapImageHandler
5
- from .rand25_handler import ReImageHandler
6
- from .config.rand25_parser import RRMapParser
7
- from .config.shared import CameraShared, CameraSharedManager
8
4
  from .config.colors import ColorsManagment
9
5
  from .config.drawable import Drawable
6
+ from .config.rand25_parser import RRMapParser
7
+ from .config.shared import CameraShared, CameraSharedManager
10
8
  from .config.types import (
11
- SnapshotStore,
12
- UserLanguageStore,
13
- RoomStore,
9
+ CameraModes,
14
10
  RoomsProperties,
11
+ RoomStore,
12
+ SnapshotStore,
15
13
  TrimCropData,
16
- CameraModes,
14
+ UserLanguageStore,
17
15
  )
16
+ from .hypfer_handler import HypferMapImageHandler
17
+ from .rand25_handler import ReImageHandler
18
18
 
19
19
  __all__ = [
20
20
  "HypferMapImageHandler",
@@ -10,8 +10,8 @@ from __future__ import annotations
10
10
 
11
11
  import math
12
12
 
13
- from PIL import ImageDraw, ImageFont
14
13
  import numpy as np
14
+ from PIL import ImageDraw, ImageFont
15
15
 
16
16
  from .types import Color, NumpyArray, PilPNG, Point, Union
17
17
 
@@ -4,13 +4,13 @@ Version 0.0.1
4
4
  """
5
5
 
6
6
  import asyncio
7
- from dataclasses import dataclass
8
7
  import json
9
8
  import logging
9
+ from dataclasses import dataclass
10
10
  from typing import Any, Dict, Tuple, Union
11
11
 
12
- from PIL import Image
13
12
  import numpy as np
13
+ from PIL import Image
14
14
 
15
15
  DEFAULT_ROOMS = 1
16
16