valetudo-map-parser 0.1.9b40__py3-none-any.whl → 0.1.9b42__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- valetudo_map_parser/__init__.py +6 -3
- valetudo_map_parser/config/auto_crop.py +1 -1
- valetudo_map_parser/config/drawable.py +155 -200
- valetudo_map_parser/config/drawable_elements.py +312 -0
- valetudo_map_parser/config/enhanced_drawable.py +447 -0
- valetudo_map_parser/config/shared.py +29 -1
- valetudo_map_parser/config/types.py +15 -7
- valetudo_map_parser/config/utils.py +410 -1
- valetudo_map_parser/hypfer_draw.py +195 -61
- valetudo_map_parser/hypfer_handler.py +344 -40
- valetudo_map_parser/rand25_handler.py +223 -38
- valetudo_map_parser-0.1.9b42.dist-info/METADATA +92 -0
- valetudo_map_parser-0.1.9b42.dist-info/RECORD +23 -0
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b42.dist-info}/WHEEL +1 -1
- valetudo_map_parser-0.1.9b40.dist-info/METADATA +0 -23
- valetudo_map_parser-0.1.9b40.dist-info/RECORD +0 -21
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b42.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b42.dist-info}/NOTICE.txt +0 -0
@@ -4,53 +4,79 @@ 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
|
13
|
+
import logging
|
11
14
|
import math
|
12
15
|
|
16
|
+
# cv2 is imported but not used directly in this file
|
17
|
+
# It's needed for other modules that import from here
|
13
18
|
import numpy as np
|
14
19
|
from PIL import ImageDraw, ImageFont
|
15
20
|
|
16
|
-
from .types import Color, NumpyArray, PilPNG, Point, Union
|
21
|
+
from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
|
22
|
+
|
23
|
+
|
24
|
+
_LOGGER = logging.getLogger(__name__)
|
17
25
|
|
18
26
|
|
19
27
|
class Drawable:
|
20
28
|
"""
|
21
29
|
Collection of drawing utility functions for the image handlers.
|
22
|
-
This class contains static methods to draw various elements on
|
23
|
-
We
|
30
|
+
This class contains static methods to draw various elements on NumPy arrays (images).
|
31
|
+
We can't use OpenCV because it is not supported by the Home Assistant OS.
|
24
32
|
"""
|
25
33
|
|
26
|
-
ERROR_OUTLINE = (0, 0, 0, 255) # Red color for error messages
|
27
|
-
ERROR_COLOR = (
|
34
|
+
ERROR_OUTLINE: Color = (0, 0, 0, 255) # Red color for error messages
|
35
|
+
ERROR_COLOR: Color = (
|
36
|
+
255,
|
37
|
+
0,
|
38
|
+
0,
|
39
|
+
191,
|
40
|
+
) # Red color with lower opacity for error outlines
|
28
41
|
|
29
42
|
@staticmethod
|
30
43
|
async def create_empty_image(
|
31
44
|
width: int, height: int, background_color: Color
|
32
45
|
) -> NumpyArray:
|
33
|
-
"""Create the empty background image
|
34
|
-
Background color is specified as RGBA tuple."""
|
35
|
-
|
36
|
-
return image_array
|
46
|
+
"""Create the empty background image NumPy array.
|
47
|
+
Background color is specified as an RGBA tuple."""
|
48
|
+
return np.full((height, width, 4), background_color, dtype=np.uint8)
|
37
49
|
|
38
50
|
@staticmethod
|
39
51
|
async def from_json_to_image(
|
40
52
|
layer: NumpyArray, pixels: Union[dict, list], pixel_size: int, color: Color
|
41
53
|
) -> NumpyArray:
|
42
|
-
"""
|
54
|
+
"""Draw the layers (rooms) from the vacuum JSON data onto the image array."""
|
43
55
|
image_array = layer
|
56
|
+
# Extract alpha from color
|
57
|
+
alpha = color[3] if len(color) == 4 else 255
|
58
|
+
|
59
|
+
# For debugging
|
60
|
+
_LOGGER.debug("Drawing with color %s and alpha %s", color, alpha)
|
61
|
+
|
62
|
+
# Create the full color with alpha
|
63
|
+
full_color = color if len(color) == 4 else (*color, 255)
|
64
|
+
|
44
65
|
# Loop through pixels to find min and max coordinates
|
45
66
|
for x, y, z in pixels:
|
46
67
|
col = x * pixel_size
|
47
68
|
row = y * pixel_size
|
48
|
-
# Draw pixels
|
69
|
+
# Draw pixels as blocks
|
49
70
|
for i in range(z):
|
50
|
-
|
71
|
+
# Get the region to update
|
72
|
+
region = image_array[
|
51
73
|
row : row + pixel_size,
|
52
74
|
col + i * pixel_size : col + (i + 1) * pixel_size,
|
53
|
-
]
|
75
|
+
]
|
76
|
+
|
77
|
+
# Simple direct assignment - ignore alpha for now to ensure visibility
|
78
|
+
region[:] = full_color
|
79
|
+
|
54
80
|
return image_array
|
55
81
|
|
56
82
|
@staticmethod
|
@@ -60,12 +86,10 @@ class Drawable:
|
|
60
86
|
"""Draw the battery charger on the input layer."""
|
61
87
|
charger_width = 10
|
62
88
|
charger_height = 20
|
63
|
-
# Get the starting and ending indices of the charger rectangle
|
64
89
|
start_row = y - charger_height // 2
|
65
90
|
end_row = start_row + charger_height
|
66
91
|
start_col = x - charger_width // 2
|
67
92
|
end_col = start_col + charger_width
|
68
|
-
# Fill in the charger rectangle with the specified color
|
69
93
|
layers[start_row:end_row, start_col:end_col] = color
|
70
94
|
return layers
|
71
95
|
|
@@ -74,13 +98,10 @@ class Drawable:
|
|
74
98
|
layer: NumpyArray, center: Point, rotation_angle: int, flag_color: Color
|
75
99
|
) -> NumpyArray:
|
76
100
|
"""
|
77
|
-
|
78
|
-
|
79
|
-
to orientate the flag on the given layer.
|
101
|
+
Draw a flag centered at specified coordinates on the input layer.
|
102
|
+
It uses the rotation angle of the image to orient the flag.
|
80
103
|
"""
|
81
|
-
#
|
82
|
-
pole_color = (0, 0, 255, 255) # RGBA color (blue)
|
83
|
-
# Define flag size and position
|
104
|
+
pole_color: Color = (0, 0, 255, 255) # Blue for the pole
|
84
105
|
flag_size = 50
|
85
106
|
pole_width = 6
|
86
107
|
# Adjust flag coordinates based on rotation angle
|
@@ -91,23 +112,16 @@ class Drawable:
|
|
91
112
|
y2 = y1 + (flag_size // 2)
|
92
113
|
x3 = center[0] + (flag_size // 2)
|
93
114
|
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)
|
115
|
+
xp1, yp1 = center[0], center[1] - (pole_width // 2)
|
116
|
+
xp2, yp2 = center[0] + flag_size, center[1] - (pole_width // 2)
|
99
117
|
elif rotation_angle == 180:
|
100
118
|
x1 = center[0]
|
101
119
|
y1 = center[1] - (flag_size // 2)
|
102
120
|
x2 = center[0] - (flag_size // 2)
|
103
121
|
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
|
122
|
+
x3, y3 = center[0], center[1]
|
123
|
+
xp1, yp1 = center[0] + (pole_width // 2), center[1] - flag_size
|
124
|
+
xp2, yp2 = center[0] + (pole_width // 2), y3
|
111
125
|
elif rotation_angle == 270:
|
112
126
|
x1 = center[0] - flag_size
|
113
127
|
y1 = center[1] + (pole_width // 2)
|
@@ -115,61 +129,40 @@ class Drawable:
|
|
115
129
|
y2 = y1 - (flag_size // 2)
|
116
130
|
x3 = center[0] - (flag_size // 2)
|
117
131
|
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
|
132
|
+
xp1, yp1 = center[0] - flag_size, center[1] + (pole_width // 2)
|
133
|
+
xp2, yp2 = center[0], center[1] + (pole_width // 2)
|
134
|
+
else: # rotation_angle == 0 (no rotation)
|
135
|
+
x1, y1 = center[0], center[1]
|
136
|
+
x2, y2 = center[0] + (flag_size // 2), center[1] + (flag_size // 4)
|
137
|
+
x3, y3 = center[0], center[1] + flag_size // 2
|
138
|
+
xp1, yp1 = center[0] - (pole_width // 2), y1
|
139
|
+
xp2, yp2 = center[0] - (pole_width // 2), center[1] + flag_size
|
136
140
|
|
137
141
|
# Draw flag outline using _polygon_outline
|
138
142
|
points = [(x1, y1), (x2, y2), (x3, y3)]
|
139
143
|
layer = Drawable._polygon_outline(layer, points, 1, flag_color, flag_color)
|
140
|
-
|
141
144
|
# Draw pole using _line
|
142
145
|
layer = Drawable._line(layer, xp1, yp1, xp2, yp2, pole_color, pole_width)
|
143
|
-
|
144
146
|
return layer
|
145
147
|
|
146
148
|
@staticmethod
|
147
|
-
def point_inside(x: int, y: int, points) -> bool:
|
149
|
+
def point_inside(x: int, y: int, points: list[Tuple[int, int]]) -> bool:
|
148
150
|
"""
|
149
151
|
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
152
|
"""
|
158
153
|
n = len(points)
|
159
154
|
inside = False
|
160
|
-
xinters = 0
|
155
|
+
xinters = 0.0
|
161
156
|
p1x, p1y = points[0]
|
162
157
|
for i in range(1, n + 1):
|
163
158
|
p2x, p2y = points[i % n]
|
164
159
|
if y > min(p1y, p2y):
|
165
|
-
if y <= max(p1y, p2y):
|
166
|
-
if
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
inside = not inside
|
160
|
+
if y <= max(p1y, p2y) and x <= max(p1x, p2x):
|
161
|
+
if p1y != p2y:
|
162
|
+
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
163
|
+
if p1x == p2x or x <= xinters:
|
164
|
+
inside = not inside
|
171
165
|
p1x, p1y = p2x, p2y
|
172
|
-
|
173
166
|
return inside
|
174
167
|
|
175
168
|
@staticmethod
|
@@ -183,47 +176,29 @@ class Drawable:
|
|
183
176
|
width: int = 3,
|
184
177
|
) -> NumpyArray:
|
185
178
|
"""
|
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.
|
179
|
+
Draw a line on a NumPy array (layer) from point A to B using Bresenham's algorithm.
|
196
180
|
"""
|
197
|
-
# Ensure the coordinates are integers
|
198
181
|
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
182
|
dx = abs(x2 - x1)
|
202
183
|
dy = abs(y2 - y1)
|
203
184
|
sx = 1 if x1 < x2 else -1
|
204
185
|
sy = 1 if y1 < y2 else -1
|
205
186
|
err = dx - dy
|
206
|
-
|
207
187
|
while True:
|
208
|
-
# Draw a rectangle
|
188
|
+
# Draw a rectangle at the current coordinates with the specified width
|
209
189
|
for i in range(-width // 2, (width + 1) // 2):
|
210
190
|
for j in range(-width // 2, (width + 1) // 2):
|
211
191
|
if 0 <= x1 + i < layer.shape[1] and 0 <= y1 + j < layer.shape[0]:
|
212
192
|
layer[y1 + j, x1 + i] = color
|
213
|
-
|
214
193
|
if x1 == x2 and y1 == y2:
|
215
194
|
break
|
216
|
-
|
217
195
|
e2 = 2 * err
|
218
|
-
|
219
196
|
if e2 > -dy:
|
220
197
|
err -= dy
|
221
198
|
x1 += sx
|
222
|
-
|
223
199
|
if e2 < dx:
|
224
200
|
err += dx
|
225
201
|
y1 += sy
|
226
|
-
|
227
202
|
return layer
|
228
203
|
|
229
204
|
@staticmethod
|
@@ -243,23 +218,20 @@ class Drawable:
|
|
243
218
|
@staticmethod
|
244
219
|
async def lines(arr: NumpyArray, coords, width: int, color: Color) -> NumpyArray:
|
245
220
|
"""
|
246
|
-
|
247
|
-
the result is our path.
|
221
|
+
Join the coordinates creating a continuous line (path).
|
248
222
|
"""
|
249
223
|
for coord in coords:
|
250
|
-
# Use Bresenham's line algorithm to get the coordinates of the line pixels
|
251
224
|
x0, y0 = coord[0]
|
252
225
|
try:
|
253
226
|
x1, y1 = coord[1]
|
254
227
|
except IndexError:
|
255
|
-
x1 = x0
|
256
|
-
y1 = y0
|
228
|
+
x1, y1 = x0, y0
|
257
229
|
dx = abs(x1 - x0)
|
258
230
|
dy = abs(y1 - y0)
|
259
231
|
sx = 1 if x0 < x1 else -1
|
260
232
|
sy = 1 if y0 < y1 else -1
|
261
233
|
err = dx - dy
|
262
|
-
line_pixels = []
|
234
|
+
line_pixels: list[Tuple[int, int]] = []
|
263
235
|
while True:
|
264
236
|
line_pixels.append((x0, y0))
|
265
237
|
if x0 == x1 and y0 == y1:
|
@@ -271,14 +243,13 @@ class Drawable:
|
|
271
243
|
if e2 < dx:
|
272
244
|
err += dx
|
273
245
|
y0 += sy
|
274
|
-
|
275
|
-
# Iterate over the line pixels and draw filled rectangles with the specified width
|
246
|
+
# Draw filled rectangles for each pixel in the line
|
276
247
|
for pixel in line_pixels:
|
277
248
|
x, y = pixel
|
278
249
|
for i in range(width):
|
279
250
|
for j in range(width):
|
280
|
-
if 0 <= x + i < arr.shape[
|
281
|
-
arr[y +
|
251
|
+
if 0 <= x + i < arr.shape[1] and 0 <= y + j < arr.shape[0]:
|
252
|
+
arr[y + j, x + i] = color
|
282
253
|
return arr
|
283
254
|
|
284
255
|
@staticmethod
|
@@ -292,30 +263,47 @@ class Drawable:
|
|
292
263
|
) -> NumpyArray:
|
293
264
|
"""
|
294
265
|
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
266
|
"""
|
305
267
|
y, x = center
|
306
268
|
rr, cc = np.ogrid[: image.shape[0], : image.shape[1]]
|
307
269
|
circle = (rr - x) ** 2 + (cc - y) ** 2 <= radius**2
|
308
270
|
image[circle] = color
|
309
271
|
if outline_width > 0:
|
310
|
-
# Create a mask for the outer circle
|
311
272
|
outer_circle = (rr - x) ** 2 + (cc - y) ** 2 <= (
|
312
273
|
radius + outline_width
|
313
274
|
) ** 2
|
314
|
-
# Create a mask for the outline by subtracting the inner circle mask from the outer circle mask
|
315
275
|
outline_mask = outer_circle & ~circle
|
316
|
-
# Fill the outline with the outline color
|
317
276
|
image[outline_mask] = outline_color
|
277
|
+
return image
|
318
278
|
|
279
|
+
@staticmethod
|
280
|
+
def _filled_circle_optimized(
|
281
|
+
image: np.ndarray,
|
282
|
+
center: Tuple[int, int],
|
283
|
+
radius: int,
|
284
|
+
color: Color,
|
285
|
+
outline_color: Color = None,
|
286
|
+
outline_width: int = 0,
|
287
|
+
) -> np.ndarray:
|
288
|
+
"""
|
289
|
+
Optimized _filled_circle ensuring dtype compatibility with uint8.
|
290
|
+
"""
|
291
|
+
x, y = center
|
292
|
+
h, w = image.shape[:2]
|
293
|
+
color_np = np.array(color, dtype=image.dtype)
|
294
|
+
outline_color_np = (
|
295
|
+
np.array(outline_color, dtype=image.dtype)
|
296
|
+
if outline_color is not None
|
297
|
+
else None
|
298
|
+
)
|
299
|
+
y_indices, x_indices = np.meshgrid(np.arange(h), np.arange(w), indexing="ij")
|
300
|
+
dist_sq = (y_indices - y) ** 2 + (x_indices - x) ** 2
|
301
|
+
circle_mask = dist_sq <= radius**2
|
302
|
+
image[circle_mask] = color_np
|
303
|
+
if outline_width > 0 and outline_color_np is not None:
|
304
|
+
outer_mask = dist_sq <= (radius + outline_width) ** 2
|
305
|
+
outline_mask = outer_mask & ~circle_mask
|
306
|
+
image[outline_mask] = outline_color_np
|
319
307
|
return image
|
320
308
|
|
321
309
|
@staticmethod
|
@@ -325,32 +313,26 @@ class Drawable:
|
|
325
313
|
"""
|
326
314
|
Draw an ellipse on the image using NumPy.
|
327
315
|
"""
|
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
316
|
x, y = center
|
332
317
|
x1, y1 = x - radius, y - radius
|
333
318
|
x2, y2 = x + radius, y + radius
|
334
|
-
|
335
|
-
|
336
|
-
return result_image
|
319
|
+
image[y1:y2, x1:x2] = color
|
320
|
+
return image
|
337
321
|
|
338
322
|
@staticmethod
|
339
323
|
def _polygon_outline(
|
340
324
|
arr: NumpyArray,
|
341
|
-
points,
|
325
|
+
points: list[Tuple[int, int]],
|
342
326
|
width: int,
|
343
327
|
outline_color: Color,
|
344
328
|
fill_color: Color = None,
|
345
329
|
) -> NumpyArray:
|
346
330
|
"""
|
347
|
-
Draw the outline of a
|
331
|
+
Draw the outline of a polygon on the array using _line, and optionally fill it.
|
348
332
|
"""
|
349
|
-
for i,
|
350
|
-
# Get the current and next points to draw a line between them
|
333
|
+
for i, _ in enumerate(points):
|
351
334
|
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
|
335
|
+
next_point = points[(i + 1) % len(points)]
|
354
336
|
arr = Drawable._line(
|
355
337
|
arr,
|
356
338
|
current_point[0],
|
@@ -360,13 +342,11 @@ class Drawable:
|
|
360
342
|
outline_color,
|
361
343
|
width,
|
362
344
|
)
|
363
|
-
# Fill the polygon area with the specified fill color
|
364
345
|
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
|
346
|
+
min_x = min(p[0] for p in points)
|
347
|
+
max_x = max(p[0] for p in points)
|
348
|
+
min_y = min(p[1] for p in points)
|
349
|
+
max_y = max(p[1] for p in points)
|
370
350
|
for x in range(min_x, max_x + 1):
|
371
351
|
for y in range(min_y, max_y + 1):
|
372
352
|
if Drawable.point_inside(x, y, points):
|
@@ -378,17 +358,14 @@ class Drawable:
|
|
378
358
|
"""
|
379
359
|
Draw the zones on the input layer.
|
380
360
|
"""
|
381
|
-
dot_radius = 1 #
|
382
|
-
dot_spacing = 4 #
|
383
|
-
# Iterate over zones
|
361
|
+
dot_radius = 1 # Number of pixels for the dot
|
362
|
+
dot_spacing = 4 # Space between dots
|
384
363
|
for zone in coordinates:
|
385
364
|
points = zone["points"]
|
386
|
-
# determinate the points to cover.
|
387
365
|
min_x = min(points[::2])
|
388
366
|
max_x = max(points[::2])
|
389
367
|
min_y = min(points[1::2])
|
390
368
|
max_y = max(points[1::2])
|
391
|
-
# Draw ellipses (dots)
|
392
369
|
for y in range(min_y, max_y, dot_spacing):
|
393
370
|
for x in range(min_x, max_x, dot_spacing):
|
394
371
|
for _ in range(dot_radius):
|
@@ -405,40 +382,29 @@ class Drawable:
|
|
405
382
|
robot_state: str | None = None,
|
406
383
|
) -> NumpyArray:
|
407
384
|
"""
|
408
|
-
|
409
|
-
this helps numpy to work faster and at lower
|
410
|
-
memory cost.
|
385
|
+
Draw the robot on a smaller array to reduce memory cost.
|
411
386
|
"""
|
412
|
-
# Create a 52*52 empty image numpy array of the background
|
413
387
|
top_left_x = x - 26
|
414
388
|
top_left_y = y - 26
|
415
389
|
bottom_right_x = top_left_x + 52
|
416
390
|
bottom_right_y = top_left_y + 52
|
417
391
|
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
392
|
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
|
393
|
+
radius = 25
|
394
|
+
r_scaled = radius // 11
|
395
|
+
r_cover = r_scaled * 12
|
396
|
+
lidar_angle = np.deg2rad(angle + 90)
|
397
|
+
r_lidar = r_scaled * 3
|
398
|
+
r_button = r_scaled * 1
|
431
399
|
if robot_state == "error":
|
432
400
|
outline = Drawable.ERROR_OUTLINE
|
433
401
|
fill = Drawable.ERROR_COLOR
|
434
402
|
else:
|
435
403
|
outline = (fill[0] // 2, fill[1] // 2, fill[2] // 2, fill[3])
|
436
|
-
# Draw the robot outline
|
437
404
|
tmp_layer = Drawable._filled_circle(
|
438
405
|
tmp_layer, (tmp_x, tmp_y), radius, fill, outline, 1
|
439
406
|
)
|
440
|
-
|
441
|
-
angle -= 90 # we remove 90 for the cover orientation
|
407
|
+
angle -= 90
|
442
408
|
a1 = ((angle + 90) - 80) / 180 * math.pi
|
443
409
|
a2 = ((angle + 90) + 80) / 180 * math.pi
|
444
410
|
x1 = int(tmp_x - r_cover * math.sin(a1))
|
@@ -446,25 +412,17 @@ class Drawable:
|
|
446
412
|
x2 = int(tmp_x - r_cover * math.sin(a2))
|
447
413
|
y2 = int(tmp_y + r_cover * math.cos(a2))
|
448
414
|
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
|
415
|
+
lidar_x = int(tmp_x + 15 * np.cos(lidar_angle))
|
416
|
+
lidar_y = int(tmp_y + 15 * np.sin(lidar_angle))
|
452
417
|
tmp_layer = Drawable._filled_circle(
|
453
418
|
tmp_layer, (lidar_x, lidar_y), r_lidar, outline
|
454
419
|
)
|
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
|
420
|
+
butt_x = int(tmp_x - 20 * np.cos(lidar_angle))
|
421
|
+
butt_y = int(tmp_y - 20 * np.sin(lidar_angle))
|
462
422
|
tmp_layer = Drawable._filled_circle(
|
463
423
|
tmp_layer, (butt_x, butt_y), r_button, outline
|
464
424
|
)
|
465
|
-
# at last overlay the new robot image to the layer in input.
|
466
425
|
layers = Drawable.overlay_robot(layers, tmp_layer, x, y)
|
467
|
-
# return the new layer as np array.
|
468
426
|
return layers
|
469
427
|
|
470
428
|
@staticmethod
|
@@ -473,50 +431,55 @@ class Drawable:
|
|
473
431
|
) -> NumpyArray:
|
474
432
|
"""
|
475
433
|
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
434
|
"""
|
482
|
-
# Calculate the dimensions of the robot image
|
483
435
|
robot_height, robot_width, _ = robot_image.shape
|
484
|
-
# Calculate the center of the robot image (in case const changes)
|
485
436
|
robot_center_x = robot_width // 2
|
486
437
|
robot_center_y = robot_height // 2
|
487
|
-
# Calculate the position to overlay the robot on the background image
|
488
438
|
top_left_x = x - robot_center_x
|
489
439
|
top_left_y = y - robot_center_y
|
490
440
|
bottom_right_x = top_left_x + robot_width
|
491
441
|
bottom_right_y = top_left_y + robot_height
|
492
|
-
# Overlay the robot on the background image
|
493
442
|
background_image[top_left_y:bottom_right_y, top_left_x:bottom_right_x] = (
|
494
443
|
robot_image
|
495
444
|
)
|
496
445
|
return background_image
|
497
446
|
|
498
447
|
@staticmethod
|
499
|
-
def
|
500
|
-
image:
|
501
|
-
|
448
|
+
def draw_filled_circle(
|
449
|
+
image: np.ndarray,
|
450
|
+
centers: Tuple[int, int],
|
451
|
+
radius: int,
|
452
|
+
color: Tuple[int, int, int, int],
|
453
|
+
) -> np.ndarray:
|
502
454
|
"""
|
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.
|
455
|
+
Draw multiple filled circles at once using a single NumPy mask.
|
509
456
|
"""
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
457
|
+
h, w = image.shape[:2]
|
458
|
+
y_indices, x_indices = np.ogrid[:h, :w] # Precompute coordinate grids
|
459
|
+
mask = np.zeros((h, w), dtype=bool)
|
460
|
+
for cx, cy in centers:
|
461
|
+
mask |= (x_indices - cx) ** 2 + (y_indices - cy) ** 2 <= radius**2
|
462
|
+
image[mask] = color
|
463
|
+
return image
|
514
464
|
|
515
|
-
|
465
|
+
@staticmethod
|
466
|
+
async def async_draw_obstacles(
|
467
|
+
image: np.ndarray, obstacle_info_list, color: Tuple[int, int, int, int]
|
468
|
+
) -> np.ndarray:
|
469
|
+
"""
|
470
|
+
Optimized async version of draw_obstacles using asyncio.gather().
|
471
|
+
"""
|
516
472
|
|
517
|
-
|
518
|
-
|
473
|
+
def extract_centers(obs_list) -> np.ndarray:
|
474
|
+
return np.array(
|
475
|
+
[[obs["points"]["x"], obs["points"]["y"]] for obs in obs_list],
|
476
|
+
dtype=np.int32,
|
477
|
+
)
|
519
478
|
|
479
|
+
centers = await asyncio.get_running_loop().run_in_executor(
|
480
|
+
None, extract_centers, obstacle_info_list
|
481
|
+
)
|
482
|
+
Drawable.draw_filled_circle(image, centers, 6, color)
|
520
483
|
return image
|
521
484
|
|
522
485
|
@staticmethod
|
@@ -528,32 +491,24 @@ class Drawable:
|
|
528
491
|
path_font: str,
|
529
492
|
position: bool,
|
530
493
|
) -> None:
|
531
|
-
"""Draw the
|
532
|
-
# Load a fonts
|
494
|
+
"""Draw the status text on the image."""
|
533
495
|
path_default_font = (
|
534
496
|
"custom_components/mqtt_vacuum_camera/utils/fonts/FiraSans.ttf"
|
535
497
|
)
|
536
498
|
default_font = ImageFont.truetype(path_default_font, size)
|
537
499
|
user_font = ImageFont.truetype(path_font, size)
|
538
|
-
# Define the text and position
|
539
500
|
if position:
|
540
501
|
x, y = 10, 10
|
541
502
|
else:
|
542
503
|
x, y = 10, image.height - 20 - size
|
543
|
-
# Create a drawing object
|
544
504
|
draw = ImageDraw.Draw(image)
|
545
|
-
# Draw the text
|
546
505
|
for text in status:
|
547
506
|
if "\u2211" in text or "\u03de" in text:
|
548
507
|
font = default_font
|
549
508
|
width = None
|
550
509
|
else:
|
551
510
|
font = user_font
|
552
|
-
|
553
|
-
if is_variable:
|
554
|
-
width = 2
|
555
|
-
else:
|
556
|
-
width = None
|
511
|
+
width = 2 if path_font.endswith("VT.ttf") else None
|
557
512
|
if width:
|
558
513
|
draw.text((x, y), text, font=font, fill=color, stroke_width=width)
|
559
514
|
else:
|