valetudo-map-parser 0.1.2__py3-none-any.whl → 0.1.3__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 +2 -0
- valetudo_map_parser/config/__init__.py +1 -0
- valetudo_map_parser/config/auto_crop.py +288 -0
- valetudo_map_parser/config/colors.py +178 -0
- valetudo_map_parser/config/drawable.py +561 -0
- valetudo_map_parser/config/shared.py +249 -0
- valetudo_map_parser/config/types.py +590 -0
- valetudo_map_parser/hypfer_draw.py +422 -0
- valetudo_map_parser/hypfer_handler.py +418 -0
- valetudo_map_parser/images_utils.py +398 -0
- valetudo_map_parser/map_data.py +510 -0
- valetudo_map_parser/py.typed +0 -0
- {valetudo_map_parser-0.1.2.dist-info → valetudo_map_parser-0.1.3.dist-info}/METADATA +1 -1
- valetudo_map_parser-0.1.3.dist-info/RECORD +17 -0
- __init__.py +0 -18
- valetudo_map_parser-0.1.2.dist-info/RECORD +0 -6
- {valetudo_map_parser-0.1.2.dist-info → valetudo_map_parser-0.1.3.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.2.dist-info → valetudo_map_parser-0.1.3.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.2.dist-info → valetudo_map_parser-0.1.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,561 @@
|
|
1
|
+
"""
|
2
|
+
Collections of Drawing Utility
|
3
|
+
Drawable is part of the Image_Handler
|
4
|
+
used functions to draw the elements on the Numpy Array
|
5
|
+
that is actually our camera frame.
|
6
|
+
Version: v2024.12.0
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
import math
|
12
|
+
|
13
|
+
from PIL import ImageDraw, ImageFont
|
14
|
+
import numpy as np
|
15
|
+
|
16
|
+
from .types import Color, NumpyArray, PilPNG, Point, Union
|
17
|
+
|
18
|
+
|
19
|
+
class Drawable:
|
20
|
+
"""
|
21
|
+
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
|
+
"""
|
25
|
+
|
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
|
+
|
29
|
+
@staticmethod
|
30
|
+
async def create_empty_image(
|
31
|
+
width: int, height: int, background_color: Color
|
32
|
+
) -> 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
|
37
|
+
|
38
|
+
@staticmethod
|
39
|
+
async def from_json_to_image(
|
40
|
+
layer: NumpyArray, pixels: Union[dict, list], pixel_size: int, color: Color
|
41
|
+
) -> NumpyArray:
|
42
|
+
"""Drawing the layers (rooms) from the vacuum json data."""
|
43
|
+
image_array = layer
|
44
|
+
# Loop through pixels to find min and max coordinates
|
45
|
+
for x, y, z in pixels:
|
46
|
+
col = x * pixel_size
|
47
|
+
row = y * pixel_size
|
48
|
+
# Draw pixels
|
49
|
+
for i in range(z):
|
50
|
+
image_array[
|
51
|
+
row : row + pixel_size,
|
52
|
+
col + i * pixel_size : col + (i + 1) * pixel_size,
|
53
|
+
] = color
|
54
|
+
return image_array
|
55
|
+
|
56
|
+
@staticmethod
|
57
|
+
async def battery_charger(
|
58
|
+
layers: NumpyArray, x: int, y: int, color: Color
|
59
|
+
) -> NumpyArray:
|
60
|
+
"""Draw the battery charger on the input layer."""
|
61
|
+
charger_width = 10
|
62
|
+
charger_height = 20
|
63
|
+
# Get the starting and ending indices of the charger rectangle
|
64
|
+
start_row = y - charger_height // 2
|
65
|
+
end_row = start_row + charger_height
|
66
|
+
start_col = x - charger_width // 2
|
67
|
+
end_col = start_col + charger_width
|
68
|
+
# Fill in the charger rectangle with the specified color
|
69
|
+
layers[start_row:end_row, start_col:end_col] = color
|
70
|
+
return layers
|
71
|
+
|
72
|
+
@staticmethod
|
73
|
+
async def go_to_flag(
|
74
|
+
layer: NumpyArray, center: Point, rotation_angle: int, flag_color: Color
|
75
|
+
) -> NumpyArray:
|
76
|
+
"""
|
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.
|
80
|
+
"""
|
81
|
+
# Define flag color
|
82
|
+
pole_color = (0, 0, 255, 255) # RGBA color (blue)
|
83
|
+
# Define flag size and position
|
84
|
+
flag_size = 50
|
85
|
+
pole_width = 6
|
86
|
+
# Adjust flag coordinates based on rotation angle
|
87
|
+
if rotation_angle == 90:
|
88
|
+
x1 = center[0] + flag_size
|
89
|
+
y1 = center[1] - (pole_width // 2)
|
90
|
+
x2 = x1 - (flag_size // 4)
|
91
|
+
y2 = y1 + (flag_size // 2)
|
92
|
+
x3 = center[0] + (flag_size // 2)
|
93
|
+
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)
|
99
|
+
elif rotation_angle == 180:
|
100
|
+
x1 = center[0]
|
101
|
+
y1 = center[1] - (flag_size // 2)
|
102
|
+
x2 = center[0] - (flag_size // 2)
|
103
|
+
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
|
111
|
+
elif rotation_angle == 270:
|
112
|
+
x1 = center[0] - flag_size
|
113
|
+
y1 = center[1] + (pole_width // 2)
|
114
|
+
x2 = x1 + (flag_size // 4)
|
115
|
+
y2 = y1 - (flag_size // 2)
|
116
|
+
x3 = center[0] - (flag_size // 2)
|
117
|
+
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
|
136
|
+
|
137
|
+
# Draw flag outline using _polygon_outline
|
138
|
+
points = [(x1, y1), (x2, y2), (x3, y3)]
|
139
|
+
layer = Drawable._polygon_outline(layer, points, 1, flag_color, flag_color)
|
140
|
+
|
141
|
+
# Draw pole using _line
|
142
|
+
layer = Drawable._line(layer, xp1, yp1, xp2, yp2, pole_color, pole_width)
|
143
|
+
|
144
|
+
return layer
|
145
|
+
|
146
|
+
@staticmethod
|
147
|
+
def point_inside(x: int, y: int, points) -> bool:
|
148
|
+
"""
|
149
|
+
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
|
+
"""
|
158
|
+
n = len(points)
|
159
|
+
inside = False
|
160
|
+
xinters = 0
|
161
|
+
p1x, p1y = points[0]
|
162
|
+
for i in range(1, n + 1):
|
163
|
+
p2x, p2y = points[i % n]
|
164
|
+
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
|
171
|
+
p1x, p1y = p2x, p2y
|
172
|
+
|
173
|
+
return inside
|
174
|
+
|
175
|
+
@staticmethod
|
176
|
+
def _line(
|
177
|
+
layer: NumpyArray,
|
178
|
+
x1: int,
|
179
|
+
y1: int,
|
180
|
+
x2: int,
|
181
|
+
y2: int,
|
182
|
+
color: Color,
|
183
|
+
width: int = 3,
|
184
|
+
) -> NumpyArray:
|
185
|
+
"""
|
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.
|
196
|
+
"""
|
197
|
+
# Ensure the coordinates are integers
|
198
|
+
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
|
+
dx = abs(x2 - x1)
|
202
|
+
dy = abs(y2 - y1)
|
203
|
+
sx = 1 if x1 < x2 else -1
|
204
|
+
sy = 1 if y1 < y2 else -1
|
205
|
+
err = dx - dy
|
206
|
+
|
207
|
+
while True:
|
208
|
+
# Draw a rectangle with the specified width at the current coordinates
|
209
|
+
for i in range(-width // 2, (width + 1) // 2):
|
210
|
+
for j in range(-width // 2, (width + 1) // 2):
|
211
|
+
if 0 <= x1 + i < layer.shape[1] and 0 <= y1 + j < layer.shape[0]:
|
212
|
+
layer[y1 + j, x1 + i] = color
|
213
|
+
|
214
|
+
if x1 == x2 and y1 == y2:
|
215
|
+
break
|
216
|
+
|
217
|
+
e2 = 2 * err
|
218
|
+
|
219
|
+
if e2 > -dy:
|
220
|
+
err -= dy
|
221
|
+
x1 += sx
|
222
|
+
|
223
|
+
if e2 < dx:
|
224
|
+
err += dx
|
225
|
+
y1 += sy
|
226
|
+
|
227
|
+
return layer
|
228
|
+
|
229
|
+
@staticmethod
|
230
|
+
async def draw_virtual_walls(
|
231
|
+
layer: NumpyArray, virtual_walls, color: Color
|
232
|
+
) -> NumpyArray:
|
233
|
+
"""
|
234
|
+
Draw virtual walls on the input layer.
|
235
|
+
"""
|
236
|
+
for wall in virtual_walls:
|
237
|
+
for i in range(0, len(wall), 4):
|
238
|
+
x1, y1, x2, y2 = wall[i : i + 4]
|
239
|
+
# Draw the virtual wall as a line with a fixed width of 6 pixels
|
240
|
+
layer = Drawable._line(layer, x1, y1, x2, y2, color, width=6)
|
241
|
+
return layer
|
242
|
+
|
243
|
+
@staticmethod
|
244
|
+
async def lines(arr: NumpyArray, coords, width: int, color: Color) -> NumpyArray:
|
245
|
+
"""
|
246
|
+
it joins the coordinates creating a continues line.
|
247
|
+
the result is our path.
|
248
|
+
"""
|
249
|
+
for coord in coords:
|
250
|
+
# Use Bresenham's line algorithm to get the coordinates of the line pixels
|
251
|
+
x0, y0 = coord[0]
|
252
|
+
try:
|
253
|
+
x1, y1 = coord[1]
|
254
|
+
except IndexError:
|
255
|
+
x1 = x0
|
256
|
+
y1 = y0
|
257
|
+
dx = abs(x1 - x0)
|
258
|
+
dy = abs(y1 - y0)
|
259
|
+
sx = 1 if x0 < x1 else -1
|
260
|
+
sy = 1 if y0 < y1 else -1
|
261
|
+
err = dx - dy
|
262
|
+
line_pixels = []
|
263
|
+
while True:
|
264
|
+
line_pixels.append((x0, y0))
|
265
|
+
if x0 == x1 and y0 == y1:
|
266
|
+
break
|
267
|
+
e2 = 2 * err
|
268
|
+
if e2 > -dy:
|
269
|
+
err -= dy
|
270
|
+
x0 += sx
|
271
|
+
if e2 < dx:
|
272
|
+
err += dx
|
273
|
+
y0 += sy
|
274
|
+
|
275
|
+
# Iterate over the line pixels and draw filled rectangles with the specified width
|
276
|
+
for pixel in line_pixels:
|
277
|
+
x, y = pixel
|
278
|
+
for i in range(width):
|
279
|
+
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
|
282
|
+
return arr
|
283
|
+
|
284
|
+
@staticmethod
|
285
|
+
def _filled_circle(
|
286
|
+
image: NumpyArray,
|
287
|
+
center: Point,
|
288
|
+
radius: int,
|
289
|
+
color: Color,
|
290
|
+
outline_color: Color = None,
|
291
|
+
outline_width: int = 0,
|
292
|
+
) -> NumpyArray:
|
293
|
+
"""
|
294
|
+
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
|
+
"""
|
305
|
+
y, x = center
|
306
|
+
rr, cc = np.ogrid[: image.shape[0], : image.shape[1]]
|
307
|
+
circle = (rr - x) ** 2 + (cc - y) ** 2 <= radius**2
|
308
|
+
image[circle] = color
|
309
|
+
if outline_width > 0:
|
310
|
+
# Create a mask for the outer circle
|
311
|
+
outer_circle = (rr - x) ** 2 + (cc - y) ** 2 <= (
|
312
|
+
radius + outline_width
|
313
|
+
) ** 2
|
314
|
+
# Create a mask for the outline by subtracting the inner circle mask from the outer circle mask
|
315
|
+
outline_mask = outer_circle & ~circle
|
316
|
+
# Fill the outline with the outline color
|
317
|
+
image[outline_mask] = outline_color
|
318
|
+
|
319
|
+
return image
|
320
|
+
|
321
|
+
@staticmethod
|
322
|
+
def _ellipse(
|
323
|
+
image: NumpyArray, center: Point, radius: int, color: Color
|
324
|
+
) -> NumpyArray:
|
325
|
+
"""
|
326
|
+
Draw an ellipse on the image using NumPy.
|
327
|
+
"""
|
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
|
+
x, y = center
|
332
|
+
x1, y1 = x - radius, y - radius
|
333
|
+
x2, y2 = x + radius, y + radius
|
334
|
+
# Draw the filled ellipse
|
335
|
+
result_image[y1:y2, x1:x2] = color
|
336
|
+
return result_image
|
337
|
+
|
338
|
+
@staticmethod
|
339
|
+
def _polygon_outline(
|
340
|
+
arr: NumpyArray,
|
341
|
+
points,
|
342
|
+
width: int,
|
343
|
+
outline_color: Color,
|
344
|
+
fill_color: Color = None,
|
345
|
+
) -> NumpyArray:
|
346
|
+
"""
|
347
|
+
Draw the outline of a filled polygon on the array using _line.
|
348
|
+
"""
|
349
|
+
for i, point in enumerate(points):
|
350
|
+
# Get the current and next points to draw a line between them
|
351
|
+
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
|
354
|
+
arr = Drawable._line(
|
355
|
+
arr,
|
356
|
+
current_point[0],
|
357
|
+
current_point[1],
|
358
|
+
next_point[0],
|
359
|
+
next_point[1],
|
360
|
+
outline_color,
|
361
|
+
width,
|
362
|
+
)
|
363
|
+
# Fill the polygon area with the specified fill color
|
364
|
+
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
|
370
|
+
for x in range(min_x, max_x + 1):
|
371
|
+
for y in range(min_y, max_y + 1):
|
372
|
+
if Drawable.point_inside(x, y, points):
|
373
|
+
arr[y, x] = fill_color
|
374
|
+
return arr
|
375
|
+
|
376
|
+
@staticmethod
|
377
|
+
async def zones(layers: NumpyArray, coordinates, color: Color) -> NumpyArray:
|
378
|
+
"""
|
379
|
+
Draw the zones on the input layer.
|
380
|
+
"""
|
381
|
+
dot_radius = 1 # number of pixels the dot should be
|
382
|
+
dot_spacing = 4 # space between dots.
|
383
|
+
# Iterate over zones
|
384
|
+
for zone in coordinates:
|
385
|
+
points = zone["points"]
|
386
|
+
# determinate the points to cover.
|
387
|
+
min_x = min(points[::2])
|
388
|
+
max_x = max(points[::2])
|
389
|
+
min_y = min(points[1::2])
|
390
|
+
max_y = max(points[1::2])
|
391
|
+
# Draw ellipses (dots)
|
392
|
+
for y in range(min_y, max_y, dot_spacing):
|
393
|
+
for x in range(min_x, max_x, dot_spacing):
|
394
|
+
for _ in range(dot_radius):
|
395
|
+
layers = Drawable._ellipse(layers, (x, y), dot_radius, color)
|
396
|
+
return layers
|
397
|
+
|
398
|
+
@staticmethod
|
399
|
+
async def robot(
|
400
|
+
layers: NumpyArray,
|
401
|
+
x: int,
|
402
|
+
y: int,
|
403
|
+
angle: float,
|
404
|
+
fill: Color,
|
405
|
+
robot_state: str | None = None,
|
406
|
+
) -> NumpyArray:
|
407
|
+
"""
|
408
|
+
We Draw the robot with in a smaller array
|
409
|
+
this helps numpy to work faster and at lower
|
410
|
+
memory cost.
|
411
|
+
"""
|
412
|
+
# Create a 52*52 empty image numpy array of the background
|
413
|
+
top_left_x = x - 26
|
414
|
+
top_left_y = y - 26
|
415
|
+
bottom_right_x = top_left_x + 52
|
416
|
+
bottom_right_y = top_left_y + 52
|
417
|
+
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
|
+
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
|
431
|
+
if robot_state == "error":
|
432
|
+
outline = Drawable.ERROR_OUTLINE
|
433
|
+
fill = Drawable.ERROR_COLOR
|
434
|
+
else:
|
435
|
+
outline = (fill[0] // 2, fill[1] // 2, fill[2] // 2, fill[3])
|
436
|
+
# Draw the robot outline
|
437
|
+
tmp_layer = Drawable._filled_circle(
|
438
|
+
tmp_layer, (tmp_x, tmp_y), radius, fill, outline, 1
|
439
|
+
)
|
440
|
+
# Draw bin cover
|
441
|
+
angle -= 90 # we remove 90 for the cover orientation
|
442
|
+
a1 = ((angle + 90) - 80) / 180 * math.pi
|
443
|
+
a2 = ((angle + 90) + 80) / 180 * math.pi
|
444
|
+
x1 = int(tmp_x - r_cover * math.sin(a1))
|
445
|
+
y1 = int(tmp_y + r_cover * math.cos(a1))
|
446
|
+
x2 = int(tmp_x - r_cover * math.sin(a2))
|
447
|
+
y2 = int(tmp_y + r_cover * math.cos(a2))
|
448
|
+
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
|
452
|
+
tmp_layer = Drawable._filled_circle(
|
453
|
+
tmp_layer, (lidar_x, lidar_y), r_lidar, outline
|
454
|
+
)
|
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
|
462
|
+
tmp_layer = Drawable._filled_circle(
|
463
|
+
tmp_layer, (butt_x, butt_y), r_button, outline
|
464
|
+
)
|
465
|
+
# at last overlay the new robot image to the layer in input.
|
466
|
+
layers = Drawable.overlay_robot(layers, tmp_layer, x, y)
|
467
|
+
# return the new layer as np array.
|
468
|
+
return layers
|
469
|
+
|
470
|
+
@staticmethod
|
471
|
+
def overlay_robot(
|
472
|
+
background_image: NumpyArray, robot_image: NumpyArray, x: int, y: int
|
473
|
+
) -> NumpyArray:
|
474
|
+
"""
|
475
|
+
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
|
+
"""
|
482
|
+
# Calculate the dimensions of the robot image
|
483
|
+
robot_height, robot_width, _ = robot_image.shape
|
484
|
+
# Calculate the center of the robot image (in case const changes)
|
485
|
+
robot_center_x = robot_width // 2
|
486
|
+
robot_center_y = robot_height // 2
|
487
|
+
# Calculate the position to overlay the robot on the background image
|
488
|
+
top_left_x = x - robot_center_x
|
489
|
+
top_left_y = y - robot_center_y
|
490
|
+
bottom_right_x = top_left_x + robot_width
|
491
|
+
bottom_right_y = top_left_y + robot_height
|
492
|
+
# Overlay the robot on the background image
|
493
|
+
background_image[top_left_y:bottom_right_y, top_left_x:bottom_right_x] = (
|
494
|
+
robot_image
|
495
|
+
)
|
496
|
+
return background_image
|
497
|
+
|
498
|
+
@staticmethod
|
499
|
+
def draw_obstacles(
|
500
|
+
image: NumpyArray, obstacle_info_list, color: Color
|
501
|
+
) -> NumpyArray:
|
502
|
+
"""
|
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.
|
509
|
+
"""
|
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"])
|
514
|
+
|
515
|
+
radius = 6
|
516
|
+
|
517
|
+
# Draw filled circle
|
518
|
+
image = Drawable._filled_circle(image, center, radius, color)
|
519
|
+
|
520
|
+
return image
|
521
|
+
|
522
|
+
@staticmethod
|
523
|
+
def status_text(
|
524
|
+
image: PilPNG,
|
525
|
+
size: int,
|
526
|
+
color: Color,
|
527
|
+
status: list[str],
|
528
|
+
path_font: str,
|
529
|
+
position: bool,
|
530
|
+
) -> None:
|
531
|
+
"""Draw the Status Test on the image."""
|
532
|
+
# Load a fonts
|
533
|
+
path_default_font = (
|
534
|
+
"custom_components/mqtt_vacuum_camera/utils/fonts/FiraSans.ttf"
|
535
|
+
)
|
536
|
+
default_font = ImageFont.truetype(path_default_font, size)
|
537
|
+
user_font = ImageFont.truetype(path_font, size)
|
538
|
+
# Define the text and position
|
539
|
+
if position:
|
540
|
+
x, y = 10, 10
|
541
|
+
else:
|
542
|
+
x, y = 10, image.height - 20 - size
|
543
|
+
# Create a drawing object
|
544
|
+
draw = ImageDraw.Draw(image)
|
545
|
+
# Draw the text
|
546
|
+
for text in status:
|
547
|
+
if "\u2211" in text or "\u03de" in text:
|
548
|
+
font = default_font
|
549
|
+
width = None
|
550
|
+
else:
|
551
|
+
font = user_font
|
552
|
+
is_variable = path_font.endswith("VT.ttf")
|
553
|
+
if is_variable:
|
554
|
+
width = 2
|
555
|
+
else:
|
556
|
+
width = None
|
557
|
+
if width:
|
558
|
+
draw.text((x, y), text, font=font, fill=color, stroke_width=width)
|
559
|
+
else:
|
560
|
+
draw.text((x, y), text, font=font, fill=color)
|
561
|
+
x += draw.textlength(text, font=default_font)
|