valetudo-map-parser 0.1.10rc7__tar.gz → 0.1.11b1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/PKG-INFO +2 -2
  2. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/__init__.py +8 -10
  3. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/drawable.py +91 -329
  4. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -2
  5. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/rand256_parser.py +51 -15
  6. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/shared.py +6 -3
  7. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/status_text/status_text.py +1 -0
  8. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/types.py +5 -4
  9. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/utils.py +31 -127
  10. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/hypfer_draw.py +0 -2
  11. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/hypfer_handler.py +13 -19
  12. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/map_data.py +15 -10
  13. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/rand256_handler.py +68 -38
  14. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/reimg_draw.py +1 -1
  15. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/rooms_handler.py +10 -10
  16. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/pyproject.toml +2 -2
  17. valetudo_map_parser-0.1.10rc7/SCR/valetudo_map_parser/config/auto_crop.py +0 -452
  18. valetudo_map_parser-0.1.10rc7/SCR/valetudo_map_parser/config/color_utils.py +0 -105
  19. valetudo_map_parser-0.1.10rc7/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -324
  20. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/LICENSE +0 -0
  21. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/NOTICE.txt +0 -0
  22. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/README.md +0 -0
  23. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  24. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/async_utils.py +0 -0
  25. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/colors.py +0 -0
  26. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/fonts/FiraSans.ttf +0 -0
  27. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/fonts/Inter-VF.ttf +0 -0
  28. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/fonts/Lato-Regular.ttf +0 -0
  29. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/fonts/MPLUSRegular.ttf +0 -0
  30. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/fonts/NotoKufiArabic-VF.ttf +0 -0
  31. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/fonts/NotoSansCJKhk-VF.ttf +0 -0
  32. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/fonts/NotoSansKhojki.ttf +0 -0
  33. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
  34. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/SCR/valetudo_map_parser/config/status_text/translations.py +0 -0
  35. {valetudo_map_parser-0.1.10rc7 → valetudo_map_parser-0.1.11b1}/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.10rc7
3
+ Version: 0.1.11b1
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 (>=0.0.4)
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
@@ -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
- # Extract alpha from color
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
- # Sample the center of the region for blending
79
- center_y = row + pixel_size // 2
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 <= center_y < image_array.shape[0]
85
- and 0 <= center_x < image_array.shape[1]
71
+ 0 <= cy < image_array.shape[0]
72
+ and 0 <= cx < image_array.shape[1]
86
73
  ):
87
- # Get blended color
88
- blended_color = ColorsManagement.sample_and_blend_color(
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
- # Use original color if out of bounds
95
- image_array[region_slice] = full_color
77
+ image_array[region_slice] = color
96
78
  else:
97
- # No blending needed, use direct assignment
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 = ColorsManagement.sample_and_blend_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
- Includes color blending for better visual integration.
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 = ColorsManagement.sample_and_blend_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 = ColorsManagement.sample_and_blend_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 outline using _polygon_outline
223
- points = [(x1, y1), (x2, y2), (x3, y3)]
224
- layer = Drawable._polygon_outline(layer, points, 1, flag_color, flag_color)
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: np.ndarray,
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
- ) -> np.ndarray:
257
- """Draw a line on a NumPy array (layer) from point A to B using Bresenham's algorithm.
258
-
259
- Args:
260
- layer: The numpy array to draw on (H, W, C)
261
- x1, y1: Start point coordinates
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, blended_color, width)
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 on the image using NumPy.
359
- Optimized to only process the bounding box of the circle.
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
- y, x = center
362
- height, width = image.shape[:2]
363
-
364
- # Calculate the bounding box of the circle
365
- min_y = max(0, y - radius - outline_width)
366
- max_y = min(height, y + radius + outline_width + 1)
367
- min_x = max(0, x - radius - outline_width)
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
- # Create masks for the circle and outline
377
- circle_mask = dist_sq <= radius**2
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
- # Apply the fill color
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
- # Draw the outline if needed
383
- if outline_width > 0 and outline_color is not None:
384
- outer_mask = dist_sq <= (radius + outline_width) ** 2
385
- outline_mask = outer_mask & ~circle_mask
386
- image[min_y:max_y, min_x:max_x][outline_mask] = outline_color
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 solid filled polygons with alpha blending using a per-zone mask.
487
- Keeps API the same; no dotted rendering.
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
- # Build mask via PIL polygon fill (fast, C-impl)
524
- mask_img = Image.new("L", (box_w, box_h), 0)
525
- draw = ImageDraw.Draw(mask_img)
526
- draw.polygon(poly_xy, fill=255)
527
- zone_mask = np.array(mask_img, dtype=bool)
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
- # Vectorized alpha blend on RGB channels only
438
+ # Optimized alpha blend - minimize temporary allocations
532
439
  region = layers[min_y : max_y + 1, min_x : max_x + 1]
533
- rgb = region[..., :3].astype(np.float32)
534
- mask3 = zone_mask[:, :, None]
535
- blended_rgb = np.where(mask3, rgb * inv_alpha + color_rgb * alpha, rgb)
536
- region[..., :3] = blended_rgb.astype(np.uint8)
537
- # Leave alpha channel unchanged to avoid stacking transparency
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 = ColorsManagement.sample_and_blend_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
 
@@ -9,8 +9,6 @@ from __future__ import annotations
9
9
  from enum import IntEnum
10
10
  from typing import Dict, List, Tuple, Union
11
11
 
12
- import numpy as np
13
-
14
12
  from .colors import DefaultColors, SupportedColor
15
13
  from .types import LOGGER
16
14