valetudo-map-parser 0.1.10rc6__tar.gz → 0.1.11b0__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.
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/PKG-INFO +2 -2
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/__init__.py +8 -10
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/drawable.py +91 -329
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -2
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/rand256_parser.py +162 -44
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/shared.py +30 -12
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/status_text/status_text.py +1 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/types.py +12 -3
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/utils.py +44 -136
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/hypfer_draw.py +0 -2
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/hypfer_handler.py +14 -22
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/map_data.py +17 -11
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/rand256_handler.py +79 -53
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/reimg_draw.py +13 -18
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/rooms_handler.py +10 -10
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/pyproject.toml +2 -2
- valetudo_map_parser-0.1.10rc6/SCR/valetudo_map_parser/config/auto_crop.py +0 -452
- valetudo_map_parser-0.1.10rc6/SCR/valetudo_map_parser/config/color_utils.py +0 -105
- valetudo_map_parser-0.1.10rc6/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -324
- valetudo_map_parser-0.1.10rc6/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -599
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/LICENSE +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/README.md +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/__init__.py +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/async_utils.py +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/colors.py +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/fonts/FiraSans.ttf +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/fonts/Inter-VF.ttf +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/fonts/Lato-Regular.ttf +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/fonts/MPLUSRegular.ttf +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/fonts/NotoKufiArabic-VF.ttf +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/fonts/NotoSansCJKhk-VF.ttf +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/fonts/NotoSansKhojki.ttf +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/config/status_text/translations.py +0 -0
- {valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: valetudo-map-parser
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.11b0
|
4
4
|
Summary: A Python library to parse Valetudo map data returning a PIL Image object.
|
5
5
|
License: Apache-2.0
|
6
6
|
License-File: LICENSE
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
14
14
|
Classifier: Programming Language :: Python :: 3.14
|
15
15
|
Requires-Dist: Pillow (>=10.3.0)
|
16
|
-
Requires-Dist: mvcrender (
|
16
|
+
Requires-Dist: mvcrender (==0.0.6)
|
17
17
|
Requires-Dist: numpy (>=1.26.4)
|
18
18
|
Requires-Dist: scipy (>=1.12.0)
|
19
19
|
Project-URL: Bug Tracker, https://github.com/sca075/Python-package-valetudo-map-parser/issues
|
{valetudo_map_parser-0.1.10rc6 → valetudo_map_parser-0.1.11b0}/SCR/valetudo_map_parser/__init__.py
RENAMED
@@ -6,27 +6,26 @@ from pathlib import Path
|
|
6
6
|
from .config.colors import ColorsManagement
|
7
7
|
from .config.drawable import Drawable
|
8
8
|
from .config.drawable_elements import DrawableElement, DrawingConfig
|
9
|
-
from .config.enhanced_drawable import EnhancedDrawable
|
10
9
|
from .config.rand256_parser import RRMapParser
|
11
10
|
from .config.shared import CameraShared, CameraSharedManager
|
11
|
+
from .config.status_text.status_text import StatusText
|
12
|
+
from .config.status_text.translations import translations as STATUS_TEXT_TRANSLATIONS
|
12
13
|
from .config.types import (
|
13
14
|
CameraModes,
|
15
|
+
ImageSize,
|
16
|
+
JsonType,
|
17
|
+
NumpyArray,
|
18
|
+
PilPNG,
|
14
19
|
RoomsProperties,
|
15
20
|
RoomStore,
|
16
21
|
SnapshotStore,
|
17
22
|
TrimCropData,
|
18
23
|
UserLanguageStore,
|
19
|
-
JsonType,
|
20
|
-
PilPNG,
|
21
|
-
NumpyArray,
|
22
|
-
ImageSize,
|
23
24
|
)
|
24
|
-
from .config.status_text.status_text import StatusText
|
25
|
-
from .config.status_text.translations import translations as STATUS_TEXT_TRANSLATIONS
|
26
25
|
from .hypfer_handler import HypferMapImageHandler
|
27
|
-
from .rand256_handler import ReImageHandler
|
28
|
-
from .rooms_handler import RoomsHandler, RandRoomsHandler
|
29
26
|
from .map_data import HyperMapData
|
27
|
+
from .rand256_handler import ReImageHandler
|
28
|
+
from .rooms_handler import RandRoomsHandler, RoomsHandler
|
30
29
|
|
31
30
|
|
32
31
|
def get_default_font_path() -> str:
|
@@ -51,7 +50,6 @@ __all__ = [
|
|
51
50
|
"Drawable",
|
52
51
|
"DrawableElement",
|
53
52
|
"DrawingConfig",
|
54
|
-
"EnhancedDrawable",
|
55
53
|
"SnapshotStore",
|
56
54
|
"UserLanguageStore",
|
57
55
|
"RoomStore",
|
@@ -14,10 +14,10 @@ import logging
|
|
14
14
|
from pathlib import Path
|
15
15
|
|
16
16
|
import numpy as np
|
17
|
+
from mvcrender.blend import get_blended_color, sample_and_blend_color
|
18
|
+
from mvcrender.draw import circle_u8, line_u8, polygon_u8
|
17
19
|
from PIL import Image, ImageDraw, ImageFont
|
18
20
|
|
19
|
-
from .color_utils import get_blended_color
|
20
|
-
from .colors import ColorsManagement
|
21
21
|
from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
|
22
22
|
|
23
23
|
|
@@ -53,49 +53,30 @@ class Drawable:
|
|
53
53
|
) -> NumpyArray:
|
54
54
|
"""Draw the layers (rooms) from the vacuum JSON data onto the image array."""
|
55
55
|
image_array = layer
|
56
|
-
|
57
|
-
alpha = color[3] if len(color) == 4 else 255
|
58
|
-
|
59
|
-
# Create the full color with alpha
|
60
|
-
full_color = color if len(color) == 4 else (*color, 255)
|
56
|
+
need_blending = color[3] < 255
|
61
57
|
|
62
|
-
# Check if we need to blend colors (alpha < 255)
|
63
|
-
need_blending = alpha < 255
|
64
|
-
|
65
|
-
# Loop through pixels to find min and max coordinates
|
66
58
|
for x, y, z in pixels:
|
67
59
|
col = x * pixel_size
|
68
60
|
row = y * pixel_size
|
69
|
-
# Draw pixels as blocks
|
70
61
|
for i in range(z):
|
71
|
-
# Get the region to update
|
72
62
|
region_slice = (
|
73
63
|
slice(row, row + pixel_size),
|
74
64
|
slice(col + i * pixel_size, col + (i + 1) * pixel_size),
|
75
65
|
)
|
76
66
|
|
77
67
|
if need_blending:
|
78
|
-
|
79
|
-
|
80
|
-
center_x = col + i * pixel_size + pixel_size // 2
|
81
|
-
|
82
|
-
# Only blend if coordinates are valid
|
68
|
+
cy = row + pixel_size // 2
|
69
|
+
cx = col + i * pixel_size + pixel_size // 2
|
83
70
|
if (
|
84
|
-
0 <=
|
85
|
-
and 0 <=
|
71
|
+
0 <= cy < image_array.shape[0]
|
72
|
+
and 0 <= cx < image_array.shape[1]
|
86
73
|
):
|
87
|
-
|
88
|
-
|
89
|
-
image_array, center_x, center_y, full_color
|
90
|
-
)
|
91
|
-
# Apply blended color to the region
|
92
|
-
image_array[region_slice] = blended_color
|
74
|
+
px = sample_and_blend_color(image_array, cx, cy, color)
|
75
|
+
image_array[region_slice] = px
|
93
76
|
else:
|
94
|
-
|
95
|
-
image_array[region_slice] = full_color
|
77
|
+
image_array[region_slice] = color
|
96
78
|
else:
|
97
|
-
|
98
|
-
image_array[region_slice] = full_color
|
79
|
+
image_array[region_slice] = color
|
99
80
|
|
100
81
|
return image_array
|
101
82
|
|
@@ -131,9 +112,7 @@ class Drawable:
|
|
131
112
|
center_x = (start_col + end_col) // 2
|
132
113
|
|
133
114
|
# Get blended color
|
134
|
-
blended_color =
|
135
|
-
layers, center_x, center_y, color
|
136
|
-
)
|
115
|
+
blended_color = sample_and_blend_color(layers, center_x, center_y, color)
|
137
116
|
|
138
117
|
# Apply blended color
|
139
118
|
layers[start_row:end_row, start_col:end_col] = blended_color
|
@@ -150,7 +129,7 @@ class Drawable:
|
|
150
129
|
"""
|
151
130
|
Draw a flag centered at specified coordinates on the input layer.
|
152
131
|
It uses the rotation angle of the image to orient the flag.
|
153
|
-
|
132
|
+
Uses mvcrender's polygon_u8 for efficient triangle drawing.
|
154
133
|
"""
|
155
134
|
# Check if coordinates are within bounds
|
156
135
|
height, width = layer.shape[:2]
|
@@ -165,9 +144,7 @@ class Drawable:
|
|
165
144
|
|
166
145
|
# Blend flag color if needed
|
167
146
|
if flag_alpha < 255:
|
168
|
-
flag_color =
|
169
|
-
layer, x, y, flag_color
|
170
|
-
)
|
147
|
+
flag_color = sample_and_blend_color(layer, x, y, flag_color)
|
171
148
|
|
172
149
|
# Create pole color with alpha
|
173
150
|
pole_color: Color = (
|
@@ -179,9 +156,7 @@ class Drawable:
|
|
179
156
|
|
180
157
|
# Blend pole color if needed
|
181
158
|
if pole_alpha < 255:
|
182
|
-
pole_color =
|
183
|
-
layer, x, y, pole_color
|
184
|
-
)
|
159
|
+
pole_color = sample_and_blend_color(layer, x, y, pole_color)
|
185
160
|
|
186
161
|
flag_size = 50
|
187
162
|
pole_width = 6
|
@@ -219,9 +194,12 @@ class Drawable:
|
|
219
194
|
xp1, yp1 = center[0] - (pole_width // 2), y1
|
220
195
|
xp2, yp2 = center[0] - (pole_width // 2), center[1] + flag_size
|
221
196
|
|
222
|
-
# Draw flag
|
223
|
-
|
224
|
-
|
197
|
+
# Draw flag triangle using mvcrender's polygon_u8 (much faster than _polygon_outline)
|
198
|
+
xs = np.array([x1, x2, x3], dtype=np.int32)
|
199
|
+
ys = np.array([y1, y2, y3], dtype=np.int32)
|
200
|
+
# Draw filled triangle with thin outline
|
201
|
+
polygon_u8(layer, xs, ys, flag_color, 1, flag_color)
|
202
|
+
|
225
203
|
# Draw pole using _line
|
226
204
|
layer = Drawable._line(layer, xp1, yp1, xp2, yp2, pole_color, pole_width)
|
227
205
|
return layer
|
@@ -246,62 +224,19 @@ class Drawable:
|
|
246
224
|
|
247
225
|
@staticmethod
|
248
226
|
def _line(
|
249
|
-
layer:
|
227
|
+
layer: NumpyArray,
|
250
228
|
x1: int,
|
251
229
|
y1: int,
|
252
230
|
x2: int,
|
253
231
|
y2: int,
|
254
232
|
color: Color,
|
255
233
|
width: int = 3,
|
256
|
-
) ->
|
257
|
-
"""
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
x2, y2: End point coordinates
|
263
|
-
color: Color to draw with (tuple or array)
|
264
|
-
width: Width of the line in pixels
|
265
|
-
"""
|
266
|
-
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
|
267
|
-
|
268
|
-
blended_color = get_blended_color(x1, y1, x2, y2, layer, color)
|
269
|
-
|
270
|
-
dx = abs(x2 - x1)
|
271
|
-
dy = abs(y2 - y1)
|
272
|
-
sx = 1 if x1 < x2 else -1
|
273
|
-
sy = 1 if y1 < y2 else -1
|
274
|
-
err = dx - dy
|
275
|
-
|
276
|
-
half_w = width // 2
|
277
|
-
h, w = layer.shape[:2]
|
278
|
-
|
279
|
-
while True:
|
280
|
-
# Draw a filled circle for thickness
|
281
|
-
yy, xx = np.ogrid[-half_w : half_w + 1, -half_w : half_w + 1]
|
282
|
-
mask = xx**2 + yy**2 <= half_w**2
|
283
|
-
y_min = max(0, y1 - half_w)
|
284
|
-
y_max = min(h, y1 + half_w + 1)
|
285
|
-
x_min = max(0, x1 - half_w)
|
286
|
-
x_max = min(w, x1 + half_w + 1)
|
287
|
-
|
288
|
-
sub_mask = mask[
|
289
|
-
(y_min - (y1 - half_w)) : (y_max - (y1 - half_w)),
|
290
|
-
(x_min - (x1 - half_w)) : (x_max - (x1 - half_w)),
|
291
|
-
]
|
292
|
-
layer[y_min:y_max, x_min:x_max][sub_mask] = blended_color
|
293
|
-
|
294
|
-
if x1 == x2 and y1 == y2:
|
295
|
-
break
|
296
|
-
|
297
|
-
e2 = 2 * err
|
298
|
-
if e2 > -dy:
|
299
|
-
err -= dy
|
300
|
-
x1 += sx
|
301
|
-
if e2 < dx:
|
302
|
-
err += dx
|
303
|
-
y1 += sy
|
304
|
-
|
234
|
+
) -> NumpyArray:
|
235
|
+
"""Segment-aware preblend, then stamp a solid line."""
|
236
|
+
width = int(max(1, width))
|
237
|
+
# Preblend once for this segment
|
238
|
+
seg = get_blended_color(int(x1), int(y1), int(x2), int(y2), layer, color)
|
239
|
+
line_u8(layer, int(x1), int(y1), int(x2), int(y2), seg, width)
|
305
240
|
return layer
|
306
241
|
|
307
242
|
@staticmethod
|
@@ -337,11 +272,8 @@ class Drawable:
|
|
337
272
|
if x0 == x1 and y0 == y1:
|
338
273
|
continue
|
339
274
|
|
340
|
-
# Get blended color for this line segment
|
341
|
-
blended_color = get_blended_color(x0, y0, x1, y1, arr, color)
|
342
|
-
|
343
275
|
# Use the optimized line drawing method
|
344
|
-
arr = Drawable._line(arr, x0, y0, x1, y1,
|
276
|
+
arr = Drawable._line(arr, x0, y0, x1, y1, color, width)
|
345
277
|
|
346
278
|
return arr
|
347
279
|
|
@@ -355,68 +287,34 @@ class Drawable:
|
|
355
287
|
outline_width: int = 0,
|
356
288
|
) -> NumpyArray:
|
357
289
|
"""
|
358
|
-
Draw a filled circle
|
359
|
-
|
290
|
+
Draw a filled circle and optional outline using mvcrender.draw.circle_u8.
|
291
|
+
If alpha<255, preblend once at the center and stamp solid.
|
360
292
|
"""
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
#
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
max_x = min(width, x + radius + outline_width + 1)
|
369
|
-
|
370
|
-
# Create coordinate arrays for the bounding box
|
371
|
-
y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
|
372
|
-
|
373
|
-
# Calculate distances from center
|
374
|
-
dist_sq = (y_indices - y) ** 2 + (x_indices - x) ** 2
|
293
|
+
cy, cx = (
|
294
|
+
int(center[0]),
|
295
|
+
int(center[1]),
|
296
|
+
) # incoming Point is (y,x) in your codebase
|
297
|
+
h, w = image.shape[:2]
|
298
|
+
if not (0 <= cx < w and 0 <= cy < h):
|
299
|
+
return image
|
375
300
|
|
376
|
-
|
377
|
-
|
301
|
+
fill_rgba = color
|
302
|
+
if fill_rgba[3] < 255:
|
303
|
+
fill_rgba = sample_and_blend_color(image, cx, cy, fill_rgba)
|
378
304
|
|
379
|
-
|
380
|
-
image[min_y:max_y, min_x:max_x][circle_mask] = color
|
305
|
+
circle_u8(image, int(cx), int(cy), int(radius), fill_rgba, -1)
|
381
306
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
307
|
+
if outline_color is not None and outline_width > 0:
|
308
|
+
out_rgba = outline_color
|
309
|
+
if out_rgba[3] < 255:
|
310
|
+
out_rgba = sample_and_blend_color(image, cx, cy, out_rgba)
|
311
|
+
# outlined stroke thickness = outline_width
|
312
|
+
circle_u8(
|
313
|
+
image, int(cx), int(cy), int(radius), out_rgba, int(outline_width)
|
314
|
+
)
|
387
315
|
|
388
316
|
return image
|
389
317
|
|
390
|
-
@staticmethod
|
391
|
-
def _filled_circle_optimized(
|
392
|
-
image: np.ndarray,
|
393
|
-
center: Tuple[int, int],
|
394
|
-
radius: int,
|
395
|
-
color: Color,
|
396
|
-
outline_color: Color = None,
|
397
|
-
outline_width: int = 0,
|
398
|
-
) -> np.ndarray:
|
399
|
-
"""
|
400
|
-
Optimized _filled_circle ensuring dtype compatibility with uint8.
|
401
|
-
"""
|
402
|
-
x, y = center
|
403
|
-
h, w = image.shape[:2]
|
404
|
-
color_np = np.array(color, dtype=image.dtype)
|
405
|
-
outline_color_np = (
|
406
|
-
np.array(outline_color, dtype=image.dtype)
|
407
|
-
if outline_color is not None
|
408
|
-
else None
|
409
|
-
)
|
410
|
-
y_indices, x_indices = np.meshgrid(np.arange(h), np.arange(w), indexing="ij")
|
411
|
-
dist_sq = (y_indices - y) ** 2 + (x_indices - x) ** 2
|
412
|
-
circle_mask = dist_sq <= radius**2
|
413
|
-
image[circle_mask] = color_np
|
414
|
-
if outline_width > 0 and outline_color_np is not None:
|
415
|
-
outer_mask = dist_sq <= (radius + outline_width) ** 2
|
416
|
-
outline_mask = outer_mask & ~circle_mask
|
417
|
-
image[outline_mask] = outline_color_np
|
418
|
-
return image
|
419
|
-
|
420
318
|
@staticmethod
|
421
319
|
def _ellipse(
|
422
320
|
image: NumpyArray, center: Point, radius: int, color: Color
|
@@ -483,17 +381,18 @@ class Drawable:
|
|
483
381
|
@staticmethod
|
484
382
|
async def zones(layers: NumpyArray, coordinates, color: Color) -> NumpyArray:
|
485
383
|
"""
|
486
|
-
Draw zones as
|
487
|
-
|
384
|
+
Draw zones as filled polygons with alpha blending using mvcrender.
|
385
|
+
Creates a mask with polygon_u8 and blends it onto the image with proper alpha.
|
386
|
+
This eliminates PIL dependency for zone drawing.
|
488
387
|
"""
|
489
388
|
if not coordinates:
|
490
389
|
return layers
|
491
390
|
|
492
391
|
height, width = layers.shape[:2]
|
493
|
-
# Precompute color and alpha
|
494
392
|
r, g, b, a = color
|
495
393
|
alpha = a / 255.0
|
496
394
|
inv_alpha = 1.0 - alpha
|
395
|
+
# Pre-allocate color array once (avoid creating it in every iteration)
|
497
396
|
color_rgb = np.array([r, g, b], dtype=np.float32)
|
498
397
|
|
499
398
|
for zone in coordinates:
|
@@ -501,6 +400,7 @@ class Drawable:
|
|
501
400
|
pts = zone["points"]
|
502
401
|
except (KeyError, TypeError):
|
503
402
|
continue
|
403
|
+
|
504
404
|
if not pts or len(pts) < 6:
|
505
405
|
continue
|
506
406
|
|
@@ -512,29 +412,48 @@ class Drawable:
|
|
512
412
|
if min_x >= max_x or min_y >= max_y:
|
513
413
|
continue
|
514
414
|
|
515
|
-
# Adjust polygon points to local bbox coordinates
|
516
|
-
poly_xy = [
|
517
|
-
(int(pts[i] - min_x), int(pts[i + 1] - min_y))
|
518
|
-
for i in range(0, len(pts), 2)
|
519
|
-
]
|
520
415
|
box_w = max_x - min_x + 1
|
521
416
|
box_h = max_y - min_y + 1
|
522
417
|
|
523
|
-
#
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
418
|
+
# Create mask using mvcrender's polygon_u8
|
419
|
+
mask_rgba = np.zeros((box_h, box_w, 4), dtype=np.uint8)
|
420
|
+
|
421
|
+
# Convert points to xs, ys arrays (adjusted to local bbox coordinates)
|
422
|
+
xs = np.array([int(pts[i] - min_x) for i in range(0, len(pts), 2)], dtype=np.int32)
|
423
|
+
ys = np.array([int(pts[i] - min_y) for i in range(1, len(pts), 2)], dtype=np.int32)
|
424
|
+
|
425
|
+
# Draw filled polygon on mask
|
426
|
+
polygon_u8(mask_rgba, xs, ys, (0, 0, 0, 0), 0, (255, 255, 255, 255))
|
427
|
+
|
428
|
+
# Extract boolean mask from first channel
|
429
|
+
zone_mask = (mask_rgba[:, :, 0] > 0)
|
430
|
+
del mask_rgba
|
431
|
+
del xs
|
432
|
+
del ys
|
433
|
+
|
528
434
|
if not np.any(zone_mask):
|
435
|
+
del zone_mask
|
529
436
|
continue
|
530
437
|
|
531
|
-
#
|
438
|
+
# Optimized alpha blend - minimize temporary allocations
|
532
439
|
region = layers[min_y : max_y + 1, min_x : max_x + 1]
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
#
|
440
|
+
|
441
|
+
# Work directly on the region's RGB channels
|
442
|
+
rgb_region = region[..., :3]
|
443
|
+
|
444
|
+
# Apply blending only where mask is True
|
445
|
+
# Use boolean indexing to avoid creating full-size temporary arrays
|
446
|
+
rgb_masked = rgb_region[zone_mask].astype(np.float32)
|
447
|
+
|
448
|
+
# Blend: new_color = old_color * (1 - alpha) + zone_color * alpha
|
449
|
+
rgb_masked *= inv_alpha
|
450
|
+
rgb_masked += color_rgb * alpha
|
451
|
+
|
452
|
+
# Write back (convert to uint8)
|
453
|
+
rgb_region[zone_mask] = rgb_masked.astype(np.uint8)
|
454
|
+
|
455
|
+
del zone_mask
|
456
|
+
del rgb_masked
|
538
457
|
|
539
458
|
return layers
|
540
459
|
|
@@ -649,161 +568,6 @@ class Drawable:
|
|
649
568
|
)
|
650
569
|
return background_image
|
651
570
|
|
652
|
-
@staticmethod
|
653
|
-
def draw_filled_circle(
|
654
|
-
image: np.ndarray,
|
655
|
-
centers: Tuple[int, int],
|
656
|
-
radius: int,
|
657
|
-
color: Tuple[int, int, int, int],
|
658
|
-
) -> np.ndarray:
|
659
|
-
"""
|
660
|
-
Draw multiple filled circles at once using a single NumPy mask.
|
661
|
-
"""
|
662
|
-
h, w = image.shape[:2]
|
663
|
-
y_indices, x_indices = np.ogrid[:h, :w] # Precompute coordinate grids
|
664
|
-
mask = np.zeros((h, w), dtype=bool)
|
665
|
-
for cx, cy in centers:
|
666
|
-
mask |= (x_indices - cx) ** 2 + (y_indices - cy) ** 2 <= radius**2
|
667
|
-
image[mask] = color
|
668
|
-
return image
|
669
|
-
|
670
|
-
@staticmethod
|
671
|
-
def batch_draw_elements(
|
672
|
-
image: np.ndarray,
|
673
|
-
elements: list,
|
674
|
-
element_type: str,
|
675
|
-
color: Color,
|
676
|
-
) -> np.ndarray:
|
677
|
-
"""
|
678
|
-
Efficiently draw multiple elements of the same type at once.
|
679
|
-
|
680
|
-
Args:
|
681
|
-
image: The image array to draw on
|
682
|
-
elements: List of element data (coordinates, etc.)
|
683
|
-
element_type: Type of element to draw ('circle', 'line', etc.)
|
684
|
-
color: Color to use for drawing
|
685
|
-
|
686
|
-
Returns:
|
687
|
-
Modified image array
|
688
|
-
"""
|
689
|
-
if not elements or len(elements) == 0:
|
690
|
-
return image
|
691
|
-
|
692
|
-
# Get image dimensions
|
693
|
-
height, width = image.shape[:2]
|
694
|
-
|
695
|
-
if element_type == "circle":
|
696
|
-
# Extract circle centers and radii
|
697
|
-
centers = []
|
698
|
-
radii = []
|
699
|
-
for elem in elements:
|
700
|
-
if isinstance(elem, dict) and "center" in elem and "radius" in elem:
|
701
|
-
centers.append(elem["center"])
|
702
|
-
radii.append(elem["radius"])
|
703
|
-
elif isinstance(elem, (list, tuple)) and len(elem) >= 3:
|
704
|
-
# Format: (x, y, radius)
|
705
|
-
centers.append((elem[0], elem[1]))
|
706
|
-
radii.append(elem[2])
|
707
|
-
|
708
|
-
# Process circles with the same radius together
|
709
|
-
for radius in set(radii):
|
710
|
-
same_radius_centers = [
|
711
|
-
centers[i] for i in range(len(centers)) if radii[i] == radius
|
712
|
-
]
|
713
|
-
if same_radius_centers:
|
714
|
-
# Create a combined mask for all circles with this radius
|
715
|
-
mask = np.zeros((height, width), dtype=bool)
|
716
|
-
for cx, cy in same_radius_centers:
|
717
|
-
if 0 <= cx < width and 0 <= cy < height:
|
718
|
-
# Calculate circle bounds
|
719
|
-
min_y = max(0, cy - radius)
|
720
|
-
max_y = min(height, cy + radius + 1)
|
721
|
-
min_x = max(0, cx - radius)
|
722
|
-
max_x = min(width, cx + radius + 1)
|
723
|
-
|
724
|
-
# Create coordinate arrays for the circle
|
725
|
-
y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
|
726
|
-
|
727
|
-
# Add this circle to the mask
|
728
|
-
circle_mask = (y_indices - cy) ** 2 + (
|
729
|
-
x_indices - cx
|
730
|
-
) ** 2 <= radius**2
|
731
|
-
mask[min_y:max_y, min_x:max_x] |= circle_mask
|
732
|
-
|
733
|
-
# Apply color to all circles at once
|
734
|
-
image[mask] = color
|
735
|
-
|
736
|
-
elif element_type == "line":
|
737
|
-
# Extract line endpoints
|
738
|
-
lines = []
|
739
|
-
widths = []
|
740
|
-
for elem in elements:
|
741
|
-
if isinstance(elem, dict) and "start" in elem and "end" in elem:
|
742
|
-
lines.append((elem["start"], elem["end"]))
|
743
|
-
widths.append(elem.get("width", 1))
|
744
|
-
elif isinstance(elem, (list, tuple)) and len(elem) >= 4:
|
745
|
-
# Format: (x1, y1, x2, y2, [width])
|
746
|
-
lines.append(((elem[0], elem[1]), (elem[2], elem[3])))
|
747
|
-
widths.append(elem[4] if len(elem) > 4 else 1)
|
748
|
-
|
749
|
-
# Process lines with the same width together
|
750
|
-
for width in set(widths):
|
751
|
-
same_width_lines = [
|
752
|
-
lines[i] for i in range(len(lines)) if widths[i] == width
|
753
|
-
]
|
754
|
-
if same_width_lines:
|
755
|
-
# Create a combined mask for all lines with this width
|
756
|
-
mask = np.zeros((height, width), dtype=bool)
|
757
|
-
|
758
|
-
# Draw all lines into the mask
|
759
|
-
for start, end in same_width_lines:
|
760
|
-
x1, y1 = start
|
761
|
-
x2, y2 = end
|
762
|
-
|
763
|
-
# Skip invalid lines
|
764
|
-
if not (
|
765
|
-
0 <= x1 < width
|
766
|
-
and 0 <= y1 < height
|
767
|
-
and 0 <= x2 < width
|
768
|
-
and 0 <= y2 < height
|
769
|
-
):
|
770
|
-
continue
|
771
|
-
|
772
|
-
# Use Bresenham's algorithm to get line points
|
773
|
-
length = max(abs(x2 - x1), abs(y2 - y1))
|
774
|
-
if length == 0:
|
775
|
-
continue
|
776
|
-
|
777
|
-
t = np.linspace(0, 1, length * 2)
|
778
|
-
x_coordinates = np.round(x1 * (1 - t) + x2 * t).astype(int)
|
779
|
-
y_coordinates = np.round(y1 * (1 - t) + y2 * t).astype(int)
|
780
|
-
|
781
|
-
# Add line points to mask
|
782
|
-
for x, y in zip(x_coordinates, y_coordinates):
|
783
|
-
if width == 1:
|
784
|
-
mask[y, x] = True
|
785
|
-
else:
|
786
|
-
# For thicker lines
|
787
|
-
half_width = width // 2
|
788
|
-
min_y = max(0, y - half_width)
|
789
|
-
max_y = min(height, y + half_width + 1)
|
790
|
-
min_x = max(0, x - half_width)
|
791
|
-
max_x = min(width, x + half_width + 1)
|
792
|
-
|
793
|
-
# Create a circular brush
|
794
|
-
y_indices, x_indices = np.ogrid[
|
795
|
-
min_y:max_y, min_x:max_x
|
796
|
-
]
|
797
|
-
brush = (y_indices - y) ** 2 + (
|
798
|
-
x_indices - x
|
799
|
-
) ** 2 <= half_width**2
|
800
|
-
mask[min_y:max_y, min_x:max_x] |= brush
|
801
|
-
|
802
|
-
# Apply color to all lines at once
|
803
|
-
image[mask] = color
|
804
|
-
|
805
|
-
return image
|
806
|
-
|
807
571
|
@staticmethod
|
808
572
|
async def async_draw_obstacles(
|
809
573
|
image: np.ndarray, obstacle_info_list, color: Color
|
@@ -835,9 +599,7 @@ class Drawable:
|
|
835
599
|
continue
|
836
600
|
|
837
601
|
if need_blending:
|
838
|
-
obs_color =
|
839
|
-
image, x, y, color
|
840
|
-
)
|
602
|
+
obs_color = sample_and_blend_color(image, x, y, color)
|
841
603
|
else:
|
842
604
|
obs_color = color
|
843
605
|
|