valetudo-map-parser 0.1.9b40__tar.gz → 0.1.9b41__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 (23) hide show
  1. valetudo_map_parser-0.1.9b41/PKG-INFO +47 -0
  2. valetudo_map_parser-0.1.9b41/README.md +26 -0
  3. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/auto_crop.py +1 -1
  4. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/drawable.py +133 -198
  5. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/shared.py +2 -1
  6. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/types.py +13 -6
  7. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/hypfer_draw.py +1 -1
  8. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/pyproject.toml +1 -1
  9. valetudo_map_parser-0.1.9b40/PKG-INFO +0 -23
  10. valetudo_map_parser-0.1.9b40/README.md +0 -2
  11. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/LICENSE +0 -0
  12. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/NOTICE.txt +0 -0
  13. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/__init__.py +0 -0
  14. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  15. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/colors.py +0 -0
  16. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/colors_man.py +0 -0
  17. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/rand25_parser.py +0 -0
  18. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/config/utils.py +0 -0
  19. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/hypfer_handler.py +0 -0
  20. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/map_data.py +0 -0
  21. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/py.typed +0 -0
  22. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/rand25_handler.py +0 -0
  23. {valetudo_map_parser-0.1.9b40 → valetudo_map_parser-0.1.9b41}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.3
2
+ Name: valetudo-map-parser
3
+ Version: 0.1.9b41
4
+ Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
+ License: Apache-2.0
6
+ Author: Sandro Cantarella
7
+ Author-email: gsca075@gmail.com
8
+ Requires-Python: >=3.12
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: Pillow (>=10.3.0)
14
+ Requires-Dist: numpy (>=1.26.4)
15
+ Project-URL: Bug Tracker, https://github.com/sca075/Python-package-valetudo-map-parser/issues
16
+ Project-URL: Changelog, https://github.com/sca075/Python-package-valetudo-map-parser/releases
17
+ Project-URL: Homepage, https://github.com/sca075/Python-package-valetudo-map-parser
18
+ Project-URL: Repository, https://github.com/sca075/Python-package-valetudo-map-parser
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Python-package-valetudo-map-parser
22
+ #
23
+
24
+ ---
25
+ ### What is it:
26
+ ❗This is an _unofficial_ project and is not created, maintained, or in any sense linked to [valetudo.cloud](https://valetudo.cloud)
27
+
28
+ Library to Covert Valetudo's Vacuums Json data in a PIL image.
29
+
30
+ ---
31
+
32
+ It is developed and used in the [MQTT Vacuum Camera](https://github.com/sca075/mqtt_vacuum_camera).
33
+ As python package this code relay on Pythons base and popular pakages such Pillow and Numpy.
34
+
35
+ This library can be import by simply using ```pip install valetudo_map_parser```.
36
+
37
+ - It is configured with a dictionary format (on GitHub repo test [sample code](https://github.com/sca075/Python-package-valetudo-map-parser/blob/main/tests/test.py)).
38
+ - It decodes Raw data provided from Rand256.
39
+ - It provides functions to decode the json data provided from the vacuums.
40
+ - It returns a Pillow PNG image.
41
+
42
+
43
+ We are still working on the final release at current this library will be fully functional with >= v0.1.9.
44
+
45
+ Anyone can contribute to make this code batter, with code, docs or simply creating issues if any faced.
46
+ In any cases please be aware that "as it is" of course apply if you decide to use it.
47
+
@@ -0,0 +1,26 @@
1
+ # Python-package-valetudo-map-parser
2
+ #
3
+
4
+ ---
5
+ ### What is it:
6
+ ❗This is an _unofficial_ project and is not created, maintained, or in any sense linked to [valetudo.cloud](https://valetudo.cloud)
7
+
8
+ Library to Covert Valetudo's Vacuums Json data in a PIL image.
9
+
10
+ ---
11
+
12
+ It is developed and used in the [MQTT Vacuum Camera](https://github.com/sca075/mqtt_vacuum_camera).
13
+ As python package this code relay on Pythons base and popular pakages such Pillow and Numpy.
14
+
15
+ This library can be import by simply using ```pip install valetudo_map_parser```.
16
+
17
+ - It is configured with a dictionary format (on GitHub repo test [sample code](https://github.com/sca075/Python-package-valetudo-map-parser/blob/main/tests/test.py)).
18
+ - It decodes Raw data provided from Rand256.
19
+ - It provides functions to decode the json data provided from the vacuums.
20
+ - It returns a Pillow PNG image.
21
+
22
+
23
+ We are still working on the final release at current this library will be fully functional with >= v0.1.9.
24
+
25
+ Anyone can contribute to make this code batter, with code, docs or simply creating issues if any faced.
26
+ In any cases please be aware that "as it is" of course apply if you decide to use it.
@@ -118,7 +118,7 @@ class AutoCrop:
118
118
  """Initialize the auto crop data."""
119
119
  _LOGGER.debug("Auto Crop Init data: %s", str(self.auto_crop))
120
120
  _LOGGER.debug(
121
- "Auto Crop Init trims data: %s", self.handler.shared.trims.to_dict()
121
+ "Auto Crop Init trims data: %r", self.handler.shared.trims.to_dict()
122
122
  )
123
123
  if not self.auto_crop: # and self.handler.shared.vacuum_state == "docked":
124
124
  self.auto_crop = await self._async_auto_crop_data(self.handler.shared.trims)
@@ -4,48 +4,54 @@ Drawable is part of the Image_Handler
4
4
  used functions to draw the elements on the Numpy Array
5
5
  that is actually our camera frame.
6
6
  Version: v2024.12.0
7
+ Refactored for clarity, consistency, and optimized parameter usage.
7
8
  """
8
9
 
9
10
  from __future__ import annotations
10
11
 
12
+ import asyncio
11
13
  import math
12
14
 
13
15
  import numpy as np
14
16
  from PIL import ImageDraw, ImageFont
15
17
 
16
- from .types import Color, NumpyArray, PilPNG, Point, Union
18
+ from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
17
19
 
18
20
 
19
21
  class Drawable:
20
22
  """
21
23
  Collection of drawing utility functions for the image handlers.
22
- This class contains static methods to draw various elements on the Numpy Arrays (images).
23
- We cant use openCV because it is not supported by the Home Assistant OS.
24
+ This class contains static methods to draw various elements on NumPy arrays (images).
25
+ We can't use OpenCV because it is not supported by the Home Assistant OS.
24
26
  """
25
27
 
26
- ERROR_OUTLINE = (0, 0, 0, 255) # Red color for error messages
27
- ERROR_COLOR = (255, 0, 0, 191) # Red color with lower opacity for error outlines
28
+ ERROR_OUTLINE: Color = (0, 0, 0, 255) # Red color for error messages
29
+ ERROR_COLOR: Color = (
30
+ 255,
31
+ 0,
32
+ 0,
33
+ 191,
34
+ ) # Red color with lower opacity for error outlines
28
35
 
29
36
  @staticmethod
30
37
  async def create_empty_image(
31
38
  width: int, height: int, background_color: Color
32
39
  ) -> NumpyArray:
33
- """Create the empty background image numpy array.
34
- Background color is specified as RGBA tuple."""
35
- image_array = np.full((height, width, 4), background_color, dtype=np.uint8)
36
- return image_array
40
+ """Create the empty background image NumPy array.
41
+ Background color is specified as an RGBA tuple."""
42
+ return np.full((height, width, 4), background_color, dtype=np.uint8)
37
43
 
38
44
  @staticmethod
39
45
  async def from_json_to_image(
40
46
  layer: NumpyArray, pixels: Union[dict, list], pixel_size: int, color: Color
41
47
  ) -> NumpyArray:
42
- """Drawing the layers (rooms) from the vacuum json data."""
48
+ """Draw the layers (rooms) from the vacuum JSON data onto the image array."""
43
49
  image_array = layer
44
50
  # Loop through pixels to find min and max coordinates
45
51
  for x, y, z in pixels:
46
52
  col = x * pixel_size
47
53
  row = y * pixel_size
48
- # Draw pixels
54
+ # Draw pixels as blocks
49
55
  for i in range(z):
50
56
  image_array[
51
57
  row : row + pixel_size,
@@ -60,12 +66,10 @@ class Drawable:
60
66
  """Draw the battery charger on the input layer."""
61
67
  charger_width = 10
62
68
  charger_height = 20
63
- # Get the starting and ending indices of the charger rectangle
64
69
  start_row = y - charger_height // 2
65
70
  end_row = start_row + charger_height
66
71
  start_col = x - charger_width // 2
67
72
  end_col = start_col + charger_width
68
- # Fill in the charger rectangle with the specified color
69
73
  layers[start_row:end_row, start_col:end_col] = color
70
74
  return layers
71
75
 
@@ -74,13 +78,10 @@ class Drawable:
74
78
  layer: NumpyArray, center: Point, rotation_angle: int, flag_color: Color
75
79
  ) -> NumpyArray:
76
80
  """
77
- It is draw a flag on centered at specified coordinates on
78
- the input layer. It uses the rotation angle of the image
79
- to orientate the flag on the given layer.
81
+ Draw a flag centered at specified coordinates on the input layer.
82
+ It uses the rotation angle of the image to orient the flag.
80
83
  """
81
- # Define flag color
82
- pole_color = (0, 0, 255, 255) # RGBA color (blue)
83
- # Define flag size and position
84
+ pole_color: Color = (0, 0, 255, 255) # Blue for the pole
84
85
  flag_size = 50
85
86
  pole_width = 6
86
87
  # Adjust flag coordinates based on rotation angle
@@ -91,23 +92,16 @@ class Drawable:
91
92
  y2 = y1 + (flag_size // 2)
92
93
  x3 = center[0] + (flag_size // 2)
93
94
  y3 = center[1] - (pole_width // 2)
94
- # Define pole end position
95
- xp1 = center[0]
96
- yp1 = center[1] - (pole_width // 2)
97
- xp2 = center[0] + flag_size
98
- yp2 = center[1] - (pole_width // 2)
95
+ xp1, yp1 = center[0], center[1] - (pole_width // 2)
96
+ xp2, yp2 = center[0] + flag_size, center[1] - (pole_width // 2)
99
97
  elif rotation_angle == 180:
100
98
  x1 = center[0]
101
99
  y1 = center[1] - (flag_size // 2)
102
100
  x2 = center[0] - (flag_size // 2)
103
101
  y2 = y1 + (flag_size // 4)
104
- x3 = center[0]
105
- y3 = center[1]
106
- # Define pole end position
107
- xp1 = center[0] + (pole_width // 2)
108
- yp1 = center[1] - flag_size
109
- xp2 = center[0] + (pole_width // 2)
110
- yp2 = y3
102
+ x3, y3 = center[0], center[1]
103
+ xp1, yp1 = center[0] + (pole_width // 2), center[1] - flag_size
104
+ xp2, yp2 = center[0] + (pole_width // 2), y3
111
105
  elif rotation_angle == 270:
112
106
  x1 = center[0] - flag_size
113
107
  y1 = center[1] + (pole_width // 2)
@@ -115,61 +109,40 @@ class Drawable:
115
109
  y2 = y1 - (flag_size // 2)
116
110
  x3 = center[0] - (flag_size // 2)
117
111
  y3 = center[1] + (pole_width // 2)
118
- # Define pole end position
119
- xp1 = center[0] - flag_size
120
- yp1 = center[1] + (pole_width // 2)
121
- xp2 = center[0]
122
- yp2 = center[1] + (pole_width // 2)
123
- else:
124
- # rotation_angle == 0 (no rotation)
125
- x1 = center[0]
126
- y1 = center[1]
127
- x2 = center[0] + (flag_size // 2)
128
- y2 = y1 + (flag_size // 4)
129
- x3 = center[0]
130
- y3 = center[1] + flag_size // 2
131
- # Define pole end position
132
- xp1 = center[0] - (pole_width // 2)
133
- yp1 = y1
134
- xp2 = center[0] - (pole_width // 2)
135
- yp2 = center[1] + flag_size
112
+ xp1, yp1 = center[0] - flag_size, center[1] + (pole_width // 2)
113
+ xp2, yp2 = center[0], center[1] + (pole_width // 2)
114
+ else: # rotation_angle == 0 (no rotation)
115
+ x1, y1 = center[0], center[1]
116
+ x2, y2 = center[0] + (flag_size // 2), center[1] + (flag_size // 4)
117
+ x3, y3 = center[0], center[1] + flag_size // 2
118
+ xp1, yp1 = center[0] - (pole_width // 2), y1
119
+ xp2, yp2 = center[0] - (pole_width // 2), center[1] + flag_size
136
120
 
137
121
  # Draw flag outline using _polygon_outline
138
122
  points = [(x1, y1), (x2, y2), (x3, y3)]
139
123
  layer = Drawable._polygon_outline(layer, points, 1, flag_color, flag_color)
140
-
141
124
  # Draw pole using _line
142
125
  layer = Drawable._line(layer, xp1, yp1, xp2, yp2, pole_color, pole_width)
143
-
144
126
  return layer
145
127
 
146
128
  @staticmethod
147
- def point_inside(x: int, y: int, points) -> bool:
129
+ def point_inside(x: int, y: int, points: list[Tuple[int, int]]) -> bool:
148
130
  """
149
131
  Check if a point (x, y) is inside a polygon defined by a list of points.
150
- Utility to establish the fill point of the geometry.
151
- Parameters:
152
- - x, y: Coordinates of the point to check.
153
- - points: List of (x, y) coordinates defining the polygon.
154
-
155
- Returns:
156
- - True if the point is inside the polygon, False otherwise.
157
132
  """
158
133
  n = len(points)
159
134
  inside = False
160
- xinters = 0
135
+ xinters = 0.0
161
136
  p1x, p1y = points[0]
162
137
  for i in range(1, n + 1):
163
138
  p2x, p2y = points[i % n]
164
139
  if y > min(p1y, p2y):
165
- if y <= max(p1y, p2y):
166
- if x <= max(p1x, p2x):
167
- if p1y != p2y:
168
- xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
169
- if p1x == p2x or x <= xinters:
170
- inside = not inside
140
+ if y <= max(p1y, p2y) and x <= max(p1x, p2x):
141
+ if p1y != p2y:
142
+ xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
143
+ if p1x == p2x or x <= xinters:
144
+ inside = not inside
171
145
  p1x, p1y = p2x, p2y
172
-
173
146
  return inside
174
147
 
175
148
  @staticmethod
@@ -183,47 +156,29 @@ class Drawable:
183
156
  width: int = 3,
184
157
  ) -> NumpyArray:
185
158
  """
186
- Draw a line on a NumPy array (layer) from point A to B.
187
- Parameters:
188
- - layer: NumPy array representing the image.
189
- - x1, y1: Starting coordinates of the line.
190
- - x2, y2: Ending coordinates of the line.
191
- - color: Color of the line (e.g., [R, G, B] or [R, G, B, A] for RGBA).
192
- - width: Width of the line (default is 3).
193
-
194
- Returns:
195
- - Modified layer with the line drawn.
159
+ Draw a line on a NumPy array (layer) from point A to B using Bresenham's algorithm.
196
160
  """
197
- # Ensure the coordinates are integers
198
161
  x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
199
-
200
- # Use Bresenham's line algorithm to get the coordinates of the line pixels
201
162
  dx = abs(x2 - x1)
202
163
  dy = abs(y2 - y1)
203
164
  sx = 1 if x1 < x2 else -1
204
165
  sy = 1 if y1 < y2 else -1
205
166
  err = dx - dy
206
-
207
167
  while True:
208
- # Draw a rectangle with the specified width at the current coordinates
168
+ # Draw a rectangle at the current coordinates with the specified width
209
169
  for i in range(-width // 2, (width + 1) // 2):
210
170
  for j in range(-width // 2, (width + 1) // 2):
211
171
  if 0 <= x1 + i < layer.shape[1] and 0 <= y1 + j < layer.shape[0]:
212
172
  layer[y1 + j, x1 + i] = color
213
-
214
173
  if x1 == x2 and y1 == y2:
215
174
  break
216
-
217
175
  e2 = 2 * err
218
-
219
176
  if e2 > -dy:
220
177
  err -= dy
221
178
  x1 += sx
222
-
223
179
  if e2 < dx:
224
180
  err += dx
225
181
  y1 += sy
226
-
227
182
  return layer
228
183
 
229
184
  @staticmethod
@@ -243,23 +198,20 @@ class Drawable:
243
198
  @staticmethod
244
199
  async def lines(arr: NumpyArray, coords, width: int, color: Color) -> NumpyArray:
245
200
  """
246
- it joins the coordinates creating a continues line.
247
- the result is our path.
201
+ Join the coordinates creating a continuous line (path).
248
202
  """
249
203
  for coord in coords:
250
- # Use Bresenham's line algorithm to get the coordinates of the line pixels
251
204
  x0, y0 = coord[0]
252
205
  try:
253
206
  x1, y1 = coord[1]
254
207
  except IndexError:
255
- x1 = x0
256
- y1 = y0
208
+ x1, y1 = x0, y0
257
209
  dx = abs(x1 - x0)
258
210
  dy = abs(y1 - y0)
259
211
  sx = 1 if x0 < x1 else -1
260
212
  sy = 1 if y0 < y1 else -1
261
213
  err = dx - dy
262
- line_pixels = []
214
+ line_pixels: list[Tuple[int, int]] = []
263
215
  while True:
264
216
  line_pixels.append((x0, y0))
265
217
  if x0 == x1 and y0 == y1:
@@ -271,14 +223,13 @@ class Drawable:
271
223
  if e2 < dx:
272
224
  err += dx
273
225
  y0 += sy
274
-
275
- # Iterate over the line pixels and draw filled rectangles with the specified width
226
+ # Draw filled rectangles for each pixel in the line
276
227
  for pixel in line_pixels:
277
228
  x, y = pixel
278
229
  for i in range(width):
279
230
  for j in range(width):
280
- if 0 <= x + i < arr.shape[0] and 0 <= y + j < arr.shape[1]:
281
- arr[y + i, x + j] = color
231
+ if 0 <= x + i < arr.shape[1] and 0 <= y + j < arr.shape[0]:
232
+ arr[y + j, x + i] = color
282
233
  return arr
283
234
 
284
235
  @staticmethod
@@ -292,30 +243,47 @@ class Drawable:
292
243
  ) -> NumpyArray:
293
244
  """
294
245
  Draw a filled circle on the image using NumPy.
295
-
296
- Parameters:
297
- - image: NumPy array representing the image.
298
- - center: Center coordinates of the circle (x, y).
299
- - radius: Radius of the circle.
300
- - color: Color of the circle (e.g., [R, G, B] or [R, G, B, A] for RGBA).
301
-
302
- Returns:
303
- - Modified image with the filled circle drawn.
304
246
  """
305
247
  y, x = center
306
248
  rr, cc = np.ogrid[: image.shape[0], : image.shape[1]]
307
249
  circle = (rr - x) ** 2 + (cc - y) ** 2 <= radius**2
308
250
  image[circle] = color
309
251
  if outline_width > 0:
310
- # Create a mask for the outer circle
311
252
  outer_circle = (rr - x) ** 2 + (cc - y) ** 2 <= (
312
253
  radius + outline_width
313
254
  ) ** 2
314
- # Create a mask for the outline by subtracting the inner circle mask from the outer circle mask
315
255
  outline_mask = outer_circle & ~circle
316
- # Fill the outline with the outline color
317
256
  image[outline_mask] = outline_color
257
+ return image
318
258
 
259
+ @staticmethod
260
+ def _filled_circle_optimized(
261
+ image: np.ndarray,
262
+ center: Tuple[int, int],
263
+ radius: int,
264
+ color: Color,
265
+ outline_color: Color = None,
266
+ outline_width: int = 0,
267
+ ) -> np.ndarray:
268
+ """
269
+ Optimized _filled_circle ensuring dtype compatibility with uint8.
270
+ """
271
+ x, y = center
272
+ h, w = image.shape[:2]
273
+ color_np = np.array(color, dtype=image.dtype)
274
+ outline_color_np = (
275
+ np.array(outline_color, dtype=image.dtype)
276
+ if outline_color is not None
277
+ else None
278
+ )
279
+ y_indices, x_indices = np.meshgrid(np.arange(h), np.arange(w), indexing="ij")
280
+ dist_sq = (y_indices - y) ** 2 + (x_indices - x) ** 2
281
+ circle_mask = dist_sq <= radius**2
282
+ image[circle_mask] = color_np
283
+ if outline_width > 0 and outline_color_np is not None:
284
+ outer_mask = dist_sq <= (radius + outline_width) ** 2
285
+ outline_mask = outer_mask & ~circle_mask
286
+ image[outline_mask] = outline_color_np
319
287
  return image
320
288
 
321
289
  @staticmethod
@@ -325,32 +293,26 @@ class Drawable:
325
293
  """
326
294
  Draw an ellipse on the image using NumPy.
327
295
  """
328
- # Create a copy of the image to avoid modifying the original
329
- result_image = image
330
- # Calculate the coordinates of the ellipse's bounding box
331
296
  x, y = center
332
297
  x1, y1 = x - radius, y - radius
333
298
  x2, y2 = x + radius, y + radius
334
- # Draw the filled ellipse
335
- result_image[y1:y2, x1:x2] = color
336
- return result_image
299
+ image[y1:y2, x1:x2] = color
300
+ return image
337
301
 
338
302
  @staticmethod
339
303
  def _polygon_outline(
340
304
  arr: NumpyArray,
341
- points,
305
+ points: list[Tuple[int, int]],
342
306
  width: int,
343
307
  outline_color: Color,
344
308
  fill_color: Color = None,
345
309
  ) -> NumpyArray:
346
310
  """
347
- Draw the outline of a filled polygon on the array using _line.
311
+ Draw the outline of a polygon on the array using _line, and optionally fill it.
348
312
  """
349
- for i, point in enumerate(points):
350
- # Get the current and next points to draw a line between them
313
+ for i, _ in enumerate(points):
351
314
  current_point = points[i]
352
- next_point = points[(i + 1) % len(points)] # Wrap around to the first point
353
- # Use the _line function to draw a line between the current and next points
315
+ next_point = points[(i + 1) % len(points)]
354
316
  arr = Drawable._line(
355
317
  arr,
356
318
  current_point[0],
@@ -360,13 +322,11 @@ class Drawable:
360
322
  outline_color,
361
323
  width,
362
324
  )
363
- # Fill the polygon area with the specified fill color
364
325
  if fill_color is not None:
365
- min_x = min(point[0] for point in points)
366
- max_x = max(point[0] for point in points)
367
- min_y = min(point[1] for point in points)
368
- max_y = max(point[1] for point in points)
369
- # check if we are inside the area and set the color
326
+ min_x = min(p[0] for p in points)
327
+ max_x = max(p[0] for p in points)
328
+ min_y = min(p[1] for p in points)
329
+ max_y = max(p[1] for p in points)
370
330
  for x in range(min_x, max_x + 1):
371
331
  for y in range(min_y, max_y + 1):
372
332
  if Drawable.point_inside(x, y, points):
@@ -378,17 +338,14 @@ class Drawable:
378
338
  """
379
339
  Draw the zones on the input layer.
380
340
  """
381
- dot_radius = 1 # number of pixels the dot should be
382
- dot_spacing = 4 # space between dots.
383
- # Iterate over zones
341
+ dot_radius = 1 # Number of pixels for the dot
342
+ dot_spacing = 4 # Space between dots
384
343
  for zone in coordinates:
385
344
  points = zone["points"]
386
- # determinate the points to cover.
387
345
  min_x = min(points[::2])
388
346
  max_x = max(points[::2])
389
347
  min_y = min(points[1::2])
390
348
  max_y = max(points[1::2])
391
- # Draw ellipses (dots)
392
349
  for y in range(min_y, max_y, dot_spacing):
393
350
  for x in range(min_x, max_x, dot_spacing):
394
351
  for _ in range(dot_radius):
@@ -405,40 +362,29 @@ class Drawable:
405
362
  robot_state: str | None = None,
406
363
  ) -> NumpyArray:
407
364
  """
408
- We Draw the robot with in a smaller array
409
- this helps numpy to work faster and at lower
410
- memory cost.
365
+ Draw the robot on a smaller array to reduce memory cost.
411
366
  """
412
- # Create a 52*52 empty image numpy array of the background
413
367
  top_left_x = x - 26
414
368
  top_left_y = y - 26
415
369
  bottom_right_x = top_left_x + 52
416
370
  bottom_right_y = top_left_y + 52
417
371
  tmp_layer = layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x].copy()
418
- # centre of the above array is used from the rest of the code.
419
- # to draw the robot.
420
372
  tmp_x, tmp_y = 26, 26
421
- # Draw Robot
422
- radius = 25 # Radius of the vacuum constant
423
- r_scaled = radius // 11 # Offset scale for placement of the objects.
424
- r_cover = r_scaled * 12 # Scale factor for cover
425
- lidar_angle = np.deg2rad(
426
- angle + 90
427
- ) # Convert angle to radians and adjust for LIDAR orientation
428
- r_lidar = r_scaled * 3 # Scale factor for the lidar
429
- r_button = r_scaled * 1 # scale factor of the button
430
- # Outline colour from fill colour
373
+ radius = 25
374
+ r_scaled = radius // 11
375
+ r_cover = r_scaled * 12
376
+ lidar_angle = np.deg2rad(angle + 90)
377
+ r_lidar = r_scaled * 3
378
+ r_button = r_scaled * 1
431
379
  if robot_state == "error":
432
380
  outline = Drawable.ERROR_OUTLINE
433
381
  fill = Drawable.ERROR_COLOR
434
382
  else:
435
383
  outline = (fill[0] // 2, fill[1] // 2, fill[2] // 2, fill[3])
436
- # Draw the robot outline
437
384
  tmp_layer = Drawable._filled_circle(
438
385
  tmp_layer, (tmp_x, tmp_y), radius, fill, outline, 1
439
386
  )
440
- # Draw bin cover
441
- angle -= 90 # we remove 90 for the cover orientation
387
+ angle -= 90
442
388
  a1 = ((angle + 90) - 80) / 180 * math.pi
443
389
  a2 = ((angle + 90) + 80) / 180 * math.pi
444
390
  x1 = int(tmp_x - r_cover * math.sin(a1))
@@ -446,25 +392,17 @@ class Drawable:
446
392
  x2 = int(tmp_x - r_cover * math.sin(a2))
447
393
  y2 = int(tmp_y + r_cover * math.cos(a2))
448
394
  tmp_layer = Drawable._line(tmp_layer, x1, y1, x2, y2, outline, width=1)
449
- # Draw Lidar
450
- lidar_x = int(tmp_x + 15 * np.cos(lidar_angle)) # Calculate LIDAR x-coordinate
451
- lidar_y = int(tmp_y + 15 * np.sin(lidar_angle)) # Calculate LIDAR y-coordinate
395
+ lidar_x = int(tmp_x + 15 * np.cos(lidar_angle))
396
+ lidar_y = int(tmp_y + 15 * np.sin(lidar_angle))
452
397
  tmp_layer = Drawable._filled_circle(
453
398
  tmp_layer, (lidar_x, lidar_y), r_lidar, outline
454
399
  )
455
- # Draw Button
456
- butt_x = int(
457
- tmp_x - 20 * np.cos(lidar_angle)
458
- ) # Calculate the button x-coordinate
459
- butt_y = int(
460
- tmp_y - 20 * np.sin(lidar_angle)
461
- ) # Calculate the button y-coordinate
400
+ butt_x = int(tmp_x - 20 * np.cos(lidar_angle))
401
+ butt_y = int(tmp_y - 20 * np.sin(lidar_angle))
462
402
  tmp_layer = Drawable._filled_circle(
463
403
  tmp_layer, (butt_x, butt_y), r_button, outline
464
404
  )
465
- # at last overlay the new robot image to the layer in input.
466
405
  layers = Drawable.overlay_robot(layers, tmp_layer, x, y)
467
- # return the new layer as np array.
468
406
  return layers
469
407
 
470
408
  @staticmethod
@@ -473,50 +411,55 @@ class Drawable:
473
411
  ) -> NumpyArray:
474
412
  """
475
413
  Overlay the robot image on the background image at the specified coordinates.
476
- @param background_image:
477
- @param robot_image:
478
- @param robot x:
479
- @param robot y:
480
- @return: robot image overlaid on the background image.
481
414
  """
482
- # Calculate the dimensions of the robot image
483
415
  robot_height, robot_width, _ = robot_image.shape
484
- # Calculate the center of the robot image (in case const changes)
485
416
  robot_center_x = robot_width // 2
486
417
  robot_center_y = robot_height // 2
487
- # Calculate the position to overlay the robot on the background image
488
418
  top_left_x = x - robot_center_x
489
419
  top_left_y = y - robot_center_y
490
420
  bottom_right_x = top_left_x + robot_width
491
421
  bottom_right_y = top_left_y + robot_height
492
- # Overlay the robot on the background image
493
422
  background_image[top_left_y:bottom_right_y, top_left_x:bottom_right_x] = (
494
423
  robot_image
495
424
  )
496
425
  return background_image
497
426
 
498
427
  @staticmethod
499
- def draw_obstacles(
500
- image: NumpyArray, obstacle_info_list, color: Color
501
- ) -> NumpyArray:
428
+ def draw_filled_circle(
429
+ image: np.ndarray,
430
+ centers: Tuple[int, int],
431
+ radius: int,
432
+ color: Tuple[int, int, int, int],
433
+ ) -> np.ndarray:
502
434
  """
503
- Draw filled circles for obstacles on the image.
504
- Parameters:
505
- - image: NumPy array representing the image.
506
- - obstacle_info_list: List of dictionaries containing obstacle information.
507
- Returns:
508
- - Modified image with filled circles for obstacles.
435
+ Draw multiple filled circles at once using a single NumPy mask.
509
436
  """
510
- for obstacle_info in obstacle_info_list:
511
- enter = obstacle_info.get("points", {})
512
- # label = obstacle_info.get("label", {})
513
- center = (enter["x"], enter["y"])
437
+ h, w = image.shape[:2]
438
+ y_indices, x_indices = np.ogrid[:h, :w] # Precompute coordinate grids
439
+ mask = np.zeros((h, w), dtype=bool)
440
+ for cx, cy in centers:
441
+ mask |= (x_indices - cx) ** 2 + (y_indices - cy) ** 2 <= radius**2
442
+ image[mask] = color
443
+ return image
514
444
 
515
- radius = 6
445
+ @staticmethod
446
+ async def async_draw_obstacles(
447
+ image: np.ndarray, obstacle_info_list, color: Tuple[int, int, int, int]
448
+ ) -> np.ndarray:
449
+ """
450
+ Optimized async version of draw_obstacles using asyncio.gather().
451
+ """
516
452
 
517
- # Draw filled circle
518
- image = Drawable._filled_circle(image, center, radius, color)
453
+ def extract_centers(obs_list) -> np.ndarray:
454
+ return np.array(
455
+ [[obs["points"]["x"], obs["points"]["y"]] for obs in obs_list],
456
+ dtype=np.int32,
457
+ )
519
458
 
459
+ centers = await asyncio.get_running_loop().run_in_executor(
460
+ None, extract_centers, obstacle_info_list
461
+ )
462
+ Drawable.draw_filled_circle(image, centers, 6, color)
520
463
  return image
521
464
 
522
465
  @staticmethod
@@ -528,32 +471,24 @@ class Drawable:
528
471
  path_font: str,
529
472
  position: bool,
530
473
  ) -> None:
531
- """Draw the Status Test on the image."""
532
- # Load a fonts
474
+ """Draw the status text on the image."""
533
475
  path_default_font = (
534
476
  "custom_components/mqtt_vacuum_camera/utils/fonts/FiraSans.ttf"
535
477
  )
536
478
  default_font = ImageFont.truetype(path_default_font, size)
537
479
  user_font = ImageFont.truetype(path_font, size)
538
- # Define the text and position
539
480
  if position:
540
481
  x, y = 10, 10
541
482
  else:
542
483
  x, y = 10, image.height - 20 - size
543
- # Create a drawing object
544
484
  draw = ImageDraw.Draw(image)
545
- # Draw the text
546
485
  for text in status:
547
486
  if "\u2211" in text or "\u03de" in text:
548
487
  font = default_font
549
488
  width = None
550
489
  else:
551
490
  font = user_font
552
- is_variable = path_font.endswith("VT.ttf")
553
- if is_variable:
554
- width = 2
555
- else:
556
- width = None
491
+ width = 2 if path_font.endswith("VT.ttf") else None
557
492
  if width:
558
493
  draw.text((x, y), text, font=font, fill=color, stroke_width=width)
559
494
  else:
@@ -125,7 +125,8 @@ class CameraShared:
125
125
 
126
126
  def reset_trims(self) -> dict:
127
127
  """Reset the trims."""
128
- return self.trims.clear()
128
+ self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"])
129
+ return self.trims
129
130
 
130
131
  async def batch_update(self, **kwargs):
131
132
  """Batch update multiple attributes."""
@@ -613,20 +613,22 @@ class CameraModes:
613
613
  class TrimsData:
614
614
  """Dataclass to store and manage trims data."""
615
615
 
616
- trim_left: int = 0
616
+ floor: str = ""
617
617
  trim_up: int = 0
618
- trim_right: int = 0
618
+ trim_left: int = 0
619
619
  trim_down: int = 0
620
+ trim_right: int = 0
620
621
 
621
622
  @classmethod
622
623
  def from_json(cls, json_data: str):
623
624
  """Create a TrimsConfig instance from a JSON string."""
624
625
  data = json.loads(json_data)
625
626
  return cls(
626
- trim_left=data.get("trim_left", 0),
627
+ floor=data.get("floor", ""),
627
628
  trim_up=data.get("trim_up", 0),
628
- trim_right=data.get("trim_right", 0),
629
+ trim_left=data.get("trim_left", 0),
629
630
  trim_down=data.get("trim_down", 0),
631
+ trim_right=data.get("trim_right", 0),
630
632
  )
631
633
 
632
634
  def to_json(self) -> str:
@@ -644,8 +646,13 @@ class TrimsData:
644
646
 
645
647
  def clear(self) -> dict:
646
648
  """Clear all the trims."""
647
- self.trim_left = 0
649
+ self.floor = ""
648
650
  self.trim_up = 0
649
- self.trim_right = 0
651
+ self.trim_left = 0
650
652
  self.trim_down = 0
653
+ self.trim_right = 0
651
654
  return asdict(self)
655
+
656
+ def self_instance(self):
657
+ """Return self instance."""
658
+ return self.self_instance()
@@ -135,7 +135,7 @@ class ImageDraw:
135
135
  # List of dictionaries containing label and points for each obstacle
136
136
  # and draw obstacles on the map
137
137
  if obstacle_positions:
138
- self.img_h.draw.draw_obstacles(
138
+ await self.img_h.draw.async_draw_obstacles(
139
139
  np_array, obstacle_positions, color_no_go
140
140
  )
141
141
  return np_array
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "valetudo-map-parser"
3
- version = "0.1.9.b40"
3
+ version = "0.1.9.b41"
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,23 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: valetudo-map-parser
3
- Version: 0.1.9b40
4
- Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
- License: Apache-2.0
6
- Author: Sandro Cantarella
7
- Author-email: gsca075@gmail.com
8
- Requires-Python: >=3.12
9
- Classifier: License :: OSI Approved :: Apache Software License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.12
12
- Classifier: Programming Language :: Python :: 3.13
13
- Requires-Dist: Pillow (>=10.3.0)
14
- Requires-Dist: numpy (>=1.26.4)
15
- Project-URL: Bug Tracker, https://github.com/sca075/Python-package-valetudo-map-parser/issues
16
- Project-URL: Changelog, https://github.com/sca075/Python-package-valetudo-map-parser/releases
17
- Project-URL: Homepage, https://github.com/sca075/Python-package-valetudo-map-parser
18
- Project-URL: Repository, https://github.com/sca075/Python-package-valetudo-map-parser
19
- Description-Content-Type: text/markdown
20
-
21
- # Python-package-valetudo-map-parser
22
- Library to Covert Valetudo's Vacuums Json data in a PIL image.
23
-
@@ -1,2 +0,0 @@
1
- # Python-package-valetudo-map-parser
2
- Library to Covert Valetudo's Vacuums Json data in a PIL image.