valetudo-map-parser 0.1.9b40__py3-none-any.whl → 0.1.9b41__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- valetudo_map_parser/config/auto_crop.py +1 -1
- valetudo_map_parser/config/drawable.py +133 -198
- valetudo_map_parser/config/shared.py +2 -1
- valetudo_map_parser/config/types.py +13 -6
- valetudo_map_parser/hypfer_draw.py +1 -1
- valetudo_map_parser-0.1.9b41.dist-info/METADATA +47 -0
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b41.dist-info}/RECORD +10 -10
- valetudo_map_parser-0.1.9b40.dist-info/METADATA +0 -23
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b41.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b41.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b41.dist-info}/WHEEL +0 -0
@@ -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: %
|
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
|
23
|
-
We
|
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 = (
|
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
|
34
|
-
Background color is specified as RGBA tuple."""
|
35
|
-
|
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
|
-
"""
|
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
|
-
|
78
|
-
|
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
|
-
#
|
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
|
-
|
95
|
-
|
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
|
-
|
106
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
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
|
-
|
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[
|
281
|
-
arr[y +
|
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
|
-
|
335
|
-
|
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
|
311
|
+
Draw the outline of a polygon on the array using _line, and optionally fill it.
|
348
312
|
"""
|
349
|
-
for i,
|
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)]
|
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(
|
366
|
-
max_x = max(
|
367
|
-
min_y = min(
|
368
|
-
max_y = max(
|
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 #
|
382
|
-
dot_spacing = 4 #
|
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
|
-
|
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
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
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
|
-
|
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
|
-
|
450
|
-
|
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
|
-
|
456
|
-
|
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
|
500
|
-
image:
|
501
|
-
|
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
|
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
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
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
|
-
|
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
|
-
|
518
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
616
|
+
floor: str = ""
|
617
617
|
trim_up: int = 0
|
618
|
-
|
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
|
-
|
627
|
+
floor=data.get("floor", ""),
|
627
628
|
trim_up=data.get("trim_up", 0),
|
628
|
-
|
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.
|
649
|
+
self.floor = ""
|
648
650
|
self.trim_up = 0
|
649
|
-
self.
|
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.
|
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
|
@@ -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
|
+
|
@@ -1,21 +1,21 @@
|
|
1
1
|
valetudo_map_parser/__init__.py,sha256=Wmd20bdI1btzMq-0x8NxGYWskTjdUmD-Fem9MTfziwU,810
|
2
2
|
valetudo_map_parser/config/__init__.py,sha256=DQ9plV3ZF_K25Dp5ZQHPDoG-40dQoJNdNi-dfNeR3Zc,48
|
3
|
-
valetudo_map_parser/config/auto_crop.py,sha256=
|
3
|
+
valetudo_map_parser/config/auto_crop.py,sha256=6OvRsWzXMXBaSEvgwpaaisNdozDKiDyTmPjknFxoUMc,12624
|
4
4
|
valetudo_map_parser/config/colors.py,sha256=IzTT9JvF12YGGJxaTiEJRuwUdCCsFCLzsR9seCDfYWs,6515
|
5
5
|
valetudo_map_parser/config/colors_man.py,sha256=9b5c6XmpMzhEiunwfIjVkOk1lDyV-UFoasACdkGXfbo,7833
|
6
|
-
valetudo_map_parser/config/drawable.py,sha256=
|
6
|
+
valetudo_map_parser/config/drawable.py,sha256=CNuNBn1LNWaL2N30tS4jNaJhD5zV4wfGxXOhu0uaMZk,17788
|
7
7
|
valetudo_map_parser/config/rand25_parser.py,sha256=kIayyqVZBfQfAMkiArzqrrj9vqZB3pkgT0Y5ufrQmGA,16448
|
8
|
-
valetudo_map_parser/config/shared.py,sha256
|
9
|
-
valetudo_map_parser/config/types.py,sha256=
|
8
|
+
valetudo_map_parser/config/shared.py,sha256=-3HPvFDu-31P14NLXxjZP1eaN1k-l7oYWWNHlRL7IeM,10142
|
9
|
+
valetudo_map_parser/config/types.py,sha256=TAqFjELB7eZTFXq7kYvcgVqOcjprutRsuoQWPj54Rjc,18358
|
10
10
|
valetudo_map_parser/config/utils.py,sha256=yPutV-FEQlDB2z3LhOOFznoprHCApF-zrQMiOtkcO-k,19198
|
11
|
-
valetudo_map_parser/hypfer_draw.py,sha256=
|
11
|
+
valetudo_map_parser/hypfer_draw.py,sha256=akeaReXXQJX2Xp4deC8fQoWpWmcKKPPBsFRNToEyZyc,14944
|
12
12
|
valetudo_map_parser/hypfer_handler.py,sha256=f_1oG69BA94sjcZ8O9Y6A1H-YyEutbvERzqnwgremvg,13001
|
13
13
|
valetudo_map_parser/map_data.py,sha256=6FbQfgxFB6E4kcOWokReJOVSekVaE1kStyhTQhAhiOg,19469
|
14
14
|
valetudo_map_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
15
|
valetudo_map_parser/rand25_handler.py,sha256=UGRy-2IIgxZ880LlD95-vGrVdUREvM2iC1vbKhlh2eY,15249
|
16
16
|
valetudo_map_parser/reimg_draw.py,sha256=V0JUASavKVnEtAhv7nOV4pjsRxZrNsjIUtctbKO8wvk,12507
|
17
|
-
valetudo_map_parser-0.1.
|
18
|
-
valetudo_map_parser-0.1.
|
19
|
-
valetudo_map_parser-0.1.
|
20
|
-
valetudo_map_parser-0.1.
|
21
|
-
valetudo_map_parser-0.1.
|
17
|
+
valetudo_map_parser-0.1.9b41.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
|
18
|
+
valetudo_map_parser-0.1.9b41.dist-info/METADATA,sha256=GFf-n6q6CxlHAC7FctF6MumKEJjXFXQKUtgGQWN5FVQ,2093
|
19
|
+
valetudo_map_parser-0.1.9b41.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
|
20
|
+
valetudo_map_parser-0.1.9b41.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
21
|
+
valetudo_map_parser-0.1.9b41.dist-info/RECORD,,
|
@@ -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
|
-
|
File without changes
|
{valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b41.dist-info}/NOTICE.txt
RENAMED
File without changes
|
File without changes
|