valetudo-map-parser 0.1.9b100__tar.gz → 0.1.10__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.9b100 → valetudo_map_parser-0.1.10}/PKG-INFO +7 -4
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/__init__.py +24 -8
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/auto_crop.py +2 -27
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/color_utils.py +3 -4
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/colors.py +2 -2
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/drawable.py +102 -153
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -2
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/fonts/FiraSans.ttf +0 -0
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/fonts/Inter-VF.ttf +0 -0
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/fonts/Lato-Regular.ttf +0 -0
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/fonts/MPLUSRegular.ttf +0 -0
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/fonts/NotoKufiArabic-VF.ttf +0 -0
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/fonts/NotoSansCJKhk-VF.ttf +0 -0
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/fonts/NotoSansKhojki.ttf +0 -0
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/rand256_parser.py +169 -44
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/shared.py +103 -101
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/status_text/status_text.py +96 -0
- valetudo_map_parser-0.1.10/SCR/valetudo_map_parser/config/status_text/translations.py +280 -0
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/types.py +42 -13
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/utils.py +221 -181
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/hypfer_draw.py +6 -169
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/hypfer_handler.py +40 -130
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/map_data.py +403 -84
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/rand256_handler.py +53 -197
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/reimg_draw.py +14 -24
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/rooms_handler.py +3 -18
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/pyproject.toml +3 -2
- valetudo_map_parser-0.1.9b100/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -324
- valetudo_map_parser-0.1.9b100/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -599
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/README.md +0 -0
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/async_utils.py +0 -0
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
- {valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/py.typed +0 -0
@@ -1,16 +1,19 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: valetudo-map-parser
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.10
|
4
4
|
Summary: A Python library to parse Valetudo map data returning a PIL Image object.
|
5
5
|
License: Apache-2.0
|
6
|
+
License-File: LICENSE
|
7
|
+
License-File: NOTICE.txt
|
6
8
|
Author: Sandro Cantarella
|
7
9
|
Author-email: gsca075@gmail.com
|
8
|
-
Requires-Python: >=3.
|
10
|
+
Requires-Python: >=3.13
|
9
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
10
12
|
Classifier: Programming Language :: Python :: 3
|
11
|
-
Classifier: Programming Language :: Python :: 3.12
|
12
13
|
Classifier: Programming Language :: Python :: 3.13
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
13
15
|
Requires-Dist: Pillow (>=10.3.0)
|
16
|
+
Requires-Dist: mvcrender (>=0.0.5)
|
14
17
|
Requires-Dist: numpy (>=1.26.4)
|
15
18
|
Requires-Dist: scipy (>=1.12.0)
|
16
19
|
Project-URL: Bug Tracker, https://github.com/sca075/Python-package-valetudo-map-parser/issues
|
{valetudo_map_parser-0.1.9b100 → valetudo_map_parser-0.1.10}/SCR/valetudo_map_parser/__init__.py
RENAMED
@@ -1,32 +1,46 @@
|
|
1
1
|
"""Valetudo map parser.
|
2
|
-
Version: 0.1.
|
2
|
+
Version: 0.1.10"""
|
3
|
+
|
4
|
+
from pathlib import Path
|
3
5
|
|
4
6
|
from .config.colors import ColorsManagement
|
5
7
|
from .config.drawable import Drawable
|
6
8
|
from .config.drawable_elements import DrawableElement, DrawingConfig
|
7
|
-
from .config.enhanced_drawable import EnhancedDrawable
|
8
9
|
from .config.rand256_parser import RRMapParser
|
9
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
|
10
13
|
from .config.types import (
|
11
14
|
CameraModes,
|
15
|
+
ImageSize,
|
16
|
+
JsonType,
|
17
|
+
NumpyArray,
|
18
|
+
PilPNG,
|
12
19
|
RoomsProperties,
|
13
20
|
RoomStore,
|
14
21
|
SnapshotStore,
|
15
22
|
TrimCropData,
|
16
23
|
UserLanguageStore,
|
17
|
-
JsonType,
|
18
|
-
PilPNG,
|
19
|
-
NumpyArray,
|
20
|
-
ImageSize,
|
21
24
|
)
|
22
25
|
from .hypfer_handler import HypferMapImageHandler
|
26
|
+
from .map_data import HyperMapData
|
23
27
|
from .rand256_handler import ReImageHandler
|
24
|
-
from .rooms_handler import
|
28
|
+
from .rooms_handler import RandRoomsHandler, RoomsHandler
|
29
|
+
|
30
|
+
|
31
|
+
def get_default_font_path() -> str:
|
32
|
+
"""Return the absolute path to the bundled default font directory.
|
33
|
+
|
34
|
+
This returns the path to the fonts folder; the caller can join a specific font file
|
35
|
+
to avoid hard-coding a particular font here.
|
36
|
+
"""
|
37
|
+
return str((Path(__file__).resolve().parent / "config" / "fonts").resolve())
|
25
38
|
|
26
39
|
|
27
40
|
__all__ = [
|
28
41
|
"RoomsHandler",
|
29
42
|
"RandRoomsHandler",
|
43
|
+
"HyperMapData",
|
30
44
|
"HypferMapImageHandler",
|
31
45
|
"ReImageHandler",
|
32
46
|
"RRMapParser",
|
@@ -36,7 +50,6 @@ __all__ = [
|
|
36
50
|
"Drawable",
|
37
51
|
"DrawableElement",
|
38
52
|
"DrawingConfig",
|
39
|
-
"EnhancedDrawable",
|
40
53
|
"SnapshotStore",
|
41
54
|
"UserLanguageStore",
|
42
55
|
"RoomStore",
|
@@ -47,4 +60,7 @@ __all__ = [
|
|
47
60
|
"PilPNG",
|
48
61
|
"NumpyArray",
|
49
62
|
"ImageSize",
|
63
|
+
"StatusText",
|
64
|
+
"STATUS_TEXT_TRANSLATIONS",
|
65
|
+
"get_default_font_path",
|
50
66
|
]
|
@@ -6,10 +6,9 @@ from __future__ import annotations
|
|
6
6
|
import logging
|
7
7
|
|
8
8
|
import numpy as np
|
9
|
-
from numpy import rot90
|
10
9
|
from scipy import ndimage
|
11
10
|
|
12
|
-
from .async_utils import AsyncNumPy
|
11
|
+
from .async_utils import AsyncNumPy
|
13
12
|
from .types import Color, NumpyArray, TrimCropData, TrimsData
|
14
13
|
from .utils import BaseHandler
|
15
14
|
|
@@ -91,7 +90,6 @@ class AutoCrop:
|
|
91
90
|
|
92
91
|
async def _async_auto_crop_data(self, tdata: TrimsData): # , tdata=None
|
93
92
|
"""Load the auto crop data from the Camera config."""
|
94
|
-
_LOGGER.debug("Auto Crop init data: %s, %s", str(tdata), str(self.auto_crop))
|
95
93
|
if not self.auto_crop:
|
96
94
|
trims_data = TrimCropData.from_dict(dict(tdata.to_dict())).to_list()
|
97
95
|
(
|
@@ -100,7 +98,6 @@ class AutoCrop:
|
|
100
98
|
self.trim_right,
|
101
99
|
self.trim_down,
|
102
100
|
) = trims_data
|
103
|
-
_LOGGER.debug("Auto Crop trims data: %s", trims_data)
|
104
101
|
if trims_data != [0, 0, 0, 0]:
|
105
102
|
self._calculate_trimmed_dimensions()
|
106
103
|
else:
|
@@ -118,10 +115,6 @@ class AutoCrop:
|
|
118
115
|
|
119
116
|
async def _init_auto_crop(self):
|
120
117
|
"""Initialize the auto crop data."""
|
121
|
-
_LOGGER.debug("Auto Crop Init data: %s", str(self.auto_crop))
|
122
|
-
_LOGGER.debug(
|
123
|
-
"Auto Crop Init trims data: %r", self.handler.shared.trims.to_dict()
|
124
|
-
)
|
125
118
|
if not self.auto_crop: # and self.handler.shared.vacuum_state == "docked":
|
126
119
|
self.auto_crop = await self._async_auto_crop_data(self.handler.shared.trims)
|
127
120
|
if self.auto_crop:
|
@@ -131,7 +124,6 @@ class AutoCrop:
|
|
131
124
|
|
132
125
|
# Fallback: Ensure auto_crop is valid
|
133
126
|
if not self.auto_crop or any(v < 0 for v in self.auto_crop):
|
134
|
-
_LOGGER.debug("Auto-crop data unavailable. Scanning full image.")
|
135
127
|
self.auto_crop = None
|
136
128
|
|
137
129
|
return self.auto_crop
|
@@ -164,14 +156,6 @@ class AutoCrop:
|
|
164
156
|
min_y, max_y = y_slice.start, y_slice.stop - 1
|
165
157
|
min_x, max_x = x_slice.start, x_slice.stop - 1
|
166
158
|
|
167
|
-
_LOGGER.debug(
|
168
|
-
"%s: Found trims max and min values (y,x) (%s, %s) (%s, %s)...",
|
169
|
-
self.handler.file_name,
|
170
|
-
int(max_y),
|
171
|
-
int(max_x),
|
172
|
-
int(min_y),
|
173
|
-
int(min_x),
|
174
|
-
)
|
175
159
|
return min_y, min_x, max_x, max_y
|
176
160
|
|
177
161
|
async def async_get_room_bounding_box(
|
@@ -247,7 +231,7 @@ class AutoCrop:
|
|
247
231
|
return None
|
248
232
|
|
249
233
|
except Exception as e:
|
250
|
-
_LOGGER.
|
234
|
+
_LOGGER.warning(
|
251
235
|
"%s: Error calculating room bounding box for '%s': %s",
|
252
236
|
self.handler.file_name,
|
253
237
|
room_name,
|
@@ -403,7 +387,6 @@ class AutoCrop:
|
|
403
387
|
try:
|
404
388
|
self.auto_crop = await self._init_auto_crop()
|
405
389
|
if (self.auto_crop is None) or (self.auto_crop == [0, 0, 0, 0]):
|
406
|
-
_LOGGER.debug("%s: Calculating auto trim box", self.handler.file_name)
|
407
390
|
# Find the coordinates of the first occurrence of a non-background color
|
408
391
|
min_y, min_x, max_x, max_y = await self.async_image_margins(
|
409
392
|
image_array, detect_colour
|
@@ -456,15 +439,7 @@ class AutoCrop:
|
|
456
439
|
# Rotate the cropped image based on the given angle
|
457
440
|
rotated = await self.async_rotate_the_image(trimmed, rotate)
|
458
441
|
del trimmed # Free memory.
|
459
|
-
_LOGGER.debug(
|
460
|
-
"%s: Auto Trim Box data: %s", self.handler.file_name, self.crop_area
|
461
|
-
)
|
462
442
|
self.handler.crop_img_size = [rotated.shape[1], rotated.shape[0]]
|
463
|
-
_LOGGER.debug(
|
464
|
-
"%s: Auto Trimmed image size: %s",
|
465
|
-
self.handler.file_name,
|
466
|
-
self.handler.crop_img_size,
|
467
|
-
)
|
468
443
|
|
469
444
|
except RuntimeError as e:
|
470
445
|
_LOGGER.warning(
|
@@ -1,8 +1,7 @@
|
|
1
1
|
"""Utility functions for color operations in the map parser."""
|
2
2
|
|
3
|
-
from typing import Optional
|
3
|
+
from typing import Optional
|
4
4
|
|
5
|
-
from .colors import ColorsManagement
|
6
5
|
from .types import Color, NumpyArray
|
7
6
|
|
8
7
|
|
@@ -36,8 +35,8 @@ def get_blended_color(
|
|
36
35
|
# Sample background at midpoint
|
37
36
|
mid_x, mid_y = (x0 + x1) // 2, (y0 + y1) // 2
|
38
37
|
if 0 <= mid_y < arr.shape[0] and 0 <= mid_x < arr.shape[1]:
|
39
|
-
return
|
40
|
-
return (0, 0, 0, 0) # Default if out of bounds
|
38
|
+
return Color(arr[mid_y, mid_x])
|
39
|
+
return Color(0, 0, 0, 0) # Default if out of bounds
|
41
40
|
|
42
41
|
# Calculate direction vector for offset sampling
|
43
42
|
dx = x1 - x0
|
@@ -250,7 +250,7 @@ class ColorsManagement:
|
|
250
250
|
List[Tuple[int, int, int, int]]: List of RGBA colors with alpha channel added.
|
251
251
|
"""
|
252
252
|
if len(alpha_channels) != len(rgb_colors):
|
253
|
-
LOGGER.
|
253
|
+
LOGGER.warning("Input lists must have the same length.")
|
254
254
|
return []
|
255
255
|
|
256
256
|
# Fast path for empty lists
|
@@ -357,7 +357,7 @@ class ColorsManagement:
|
|
357
357
|
self.color_cache.clear()
|
358
358
|
|
359
359
|
except (ValueError, IndexError, UnboundLocalError) as e:
|
360
|
-
LOGGER.
|
360
|
+
LOGGER.warning("Error while populating colors: %s", e)
|
361
361
|
|
362
362
|
def initialize_user_colors(self, device_info: dict) -> List[Color]:
|
363
363
|
"""
|
@@ -11,13 +11,13 @@ Optimized with NumPy and SciPy for better performance.
|
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
13
|
import logging
|
14
|
-
import
|
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
|
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
|
|
@@ -85,7 +85,7 @@ class Drawable:
|
|
85
85
|
and 0 <= center_x < image_array.shape[1]
|
86
86
|
):
|
87
87
|
# Get blended color
|
88
|
-
blended_color =
|
88
|
+
blended_color = sample_and_blend_color(
|
89
89
|
image_array, center_x, center_y, full_color
|
90
90
|
)
|
91
91
|
# Apply blended color to the region
|
@@ -131,9 +131,7 @@ class Drawable:
|
|
131
131
|
center_x = (start_col + end_col) // 2
|
132
132
|
|
133
133
|
# Get blended color
|
134
|
-
blended_color =
|
135
|
-
layers, center_x, center_y, color
|
136
|
-
)
|
134
|
+
blended_color = sample_and_blend_color(layers, center_x, center_y, color)
|
137
135
|
|
138
136
|
# Apply blended color
|
139
137
|
layers[start_row:end_row, start_col:end_col] = blended_color
|
@@ -160,23 +158,24 @@ class Drawable:
|
|
160
158
|
|
161
159
|
# Get blended colors for flag and pole
|
162
160
|
flag_alpha = flag_color[3] if len(flag_color) == 4 else 255
|
163
|
-
pole_color_base =
|
161
|
+
pole_color_base = [0, 0, 255] # Blue for the pole
|
164
162
|
pole_alpha = 255
|
165
163
|
|
166
164
|
# Blend flag color if needed
|
167
165
|
if flag_alpha < 255:
|
168
|
-
flag_color =
|
169
|
-
layer, x, y, flag_color
|
170
|
-
)
|
166
|
+
flag_color = sample_and_blend_color(layer, x, y, flag_color)
|
171
167
|
|
172
168
|
# Create pole color with alpha
|
173
|
-
pole_color: Color = (
|
169
|
+
pole_color: Color = (
|
170
|
+
pole_color_base[0],
|
171
|
+
pole_color_base[1],
|
172
|
+
pole_color_base[2],
|
173
|
+
pole_alpha,
|
174
|
+
)
|
174
175
|
|
175
176
|
# Blend pole color if needed
|
176
177
|
if pole_alpha < 255:
|
177
|
-
pole_color =
|
178
|
-
layer, x, y, pole_color
|
179
|
-
)
|
178
|
+
pole_color = sample_and_blend_color(layer, x, y, pole_color)
|
180
179
|
|
181
180
|
flag_size = 50
|
182
181
|
pole_width = 6
|
@@ -223,83 +222,37 @@ class Drawable:
|
|
223
222
|
|
224
223
|
@staticmethod
|
225
224
|
def point_inside(x: int, y: int, points: list[Tuple[int, int]]) -> bool:
|
226
|
-
"""
|
227
|
-
Check if a point (x, y) is inside a polygon defined by a list of points.
|
228
|
-
"""
|
225
|
+
"""Check if a point (x, y) is inside a polygon defined by a list of points."""
|
229
226
|
n = len(points)
|
230
227
|
inside = False
|
231
|
-
|
228
|
+
inters_x = 0.0
|
232
229
|
p1x, p1y = points[0]
|
233
230
|
for i in range(1, n + 1):
|
234
231
|
p2x, p2y = points[i % n]
|
235
232
|
if y > min(p1y, p2y):
|
236
233
|
if y <= max(p1y, p2y) and x <= max(p1x, p2x):
|
237
234
|
if p1y != p2y:
|
238
|
-
|
239
|
-
if p1x == p2x or x <=
|
235
|
+
inters_x = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
236
|
+
if p1x == p2x or x <= inters_x:
|
240
237
|
inside = not inside
|
241
238
|
p1x, p1y = p2x, p2y
|
242
239
|
return inside
|
243
240
|
|
244
241
|
@staticmethod
|
245
242
|
def _line(
|
246
|
-
layer:
|
243
|
+
layer: NumpyArray,
|
247
244
|
x1: int,
|
248
245
|
y1: int,
|
249
246
|
x2: int,
|
250
247
|
y2: int,
|
251
248
|
color: Color,
|
252
249
|
width: int = 3,
|
253
|
-
) ->
|
254
|
-
"""
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
x1, y1: Start point coordinates
|
260
|
-
x2, y2: End point coordinates
|
261
|
-
color: Color to draw with (tuple or array)
|
262
|
-
width: Width of the line in pixels
|
263
|
-
"""
|
264
|
-
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
|
265
|
-
|
266
|
-
blended_color = get_blended_color(x1, y1, x2, y2, layer, color)
|
267
|
-
|
268
|
-
dx = abs(x2 - x1)
|
269
|
-
dy = abs(y2 - y1)
|
270
|
-
sx = 1 if x1 < x2 else -1
|
271
|
-
sy = 1 if y1 < y2 else -1
|
272
|
-
err = dx - dy
|
273
|
-
|
274
|
-
half_w = width // 2
|
275
|
-
h, w = layer.shape[:2]
|
276
|
-
|
277
|
-
while True:
|
278
|
-
# Draw a filled circle for thickness
|
279
|
-
yy, xx = np.ogrid[-half_w : half_w + 1, -half_w : half_w + 1]
|
280
|
-
mask = xx**2 + yy**2 <= half_w**2
|
281
|
-
y_min = max(0, y1 - half_w)
|
282
|
-
y_max = min(h, y1 + half_w + 1)
|
283
|
-
x_min = max(0, x1 - half_w)
|
284
|
-
x_max = min(w, x1 + half_w + 1)
|
285
|
-
|
286
|
-
submask = mask[
|
287
|
-
(y_min - (y1 - half_w)) : (y_max - (y1 - half_w)),
|
288
|
-
(x_min - (x1 - half_w)) : (x_max - (x1 - half_w)),
|
289
|
-
]
|
290
|
-
layer[y_min:y_max, x_min:x_max][submask] = blended_color
|
291
|
-
|
292
|
-
if x1 == x2 and y1 == y2:
|
293
|
-
break
|
294
|
-
|
295
|
-
e2 = 2 * err
|
296
|
-
if e2 > -dy:
|
297
|
-
err -= dy
|
298
|
-
x1 += sx
|
299
|
-
if e2 < dx:
|
300
|
-
err += dx
|
301
|
-
y1 += sy
|
302
|
-
|
250
|
+
) -> NumpyArray:
|
251
|
+
"""Segment-aware preblend, then stamp a solid line."""
|
252
|
+
width = int(max(1, width))
|
253
|
+
# Preblend once for this segment
|
254
|
+
seg = get_blended_color(int(x1), int(y1), int(x2), int(y2), layer, color)
|
255
|
+
line_u8(layer, int(x1), int(y1), int(x2), int(y2), seg, width)
|
303
256
|
return layer
|
304
257
|
|
305
258
|
@staticmethod
|
@@ -317,12 +270,14 @@ class Drawable:
|
|
317
270
|
return layer
|
318
271
|
|
319
272
|
@staticmethod
|
320
|
-
async def lines(
|
273
|
+
async def lines(
|
274
|
+
arr: NumpyArray, coordinates, width: int, color: Color
|
275
|
+
) -> NumpyArray:
|
321
276
|
"""
|
322
277
|
Join the coordinates creating a continuous line (path).
|
323
278
|
Optimized with vectorized operations for better performance.
|
324
279
|
"""
|
325
|
-
for coord in
|
280
|
+
for coord in coordinates:
|
326
281
|
x0, y0 = coord[0]
|
327
282
|
try:
|
328
283
|
x1, y1 = coord[1]
|
@@ -333,11 +288,8 @@ class Drawable:
|
|
333
288
|
if x0 == x1 and y0 == y1:
|
334
289
|
continue
|
335
290
|
|
336
|
-
# Get blended color for this line segment
|
337
|
-
blended_color = get_blended_color(x0, y0, x1, y1, arr, color)
|
338
|
-
|
339
291
|
# Use the optimized line drawing method
|
340
|
-
arr = Drawable._line(arr, x0, y0, x1, y1,
|
292
|
+
arr = Drawable._line(arr, x0, y0, x1, y1, color, width)
|
341
293
|
|
342
294
|
return arr
|
343
295
|
|
@@ -351,35 +303,31 @@ class Drawable:
|
|
351
303
|
outline_width: int = 0,
|
352
304
|
) -> NumpyArray:
|
353
305
|
"""
|
354
|
-
Draw a filled circle
|
355
|
-
|
306
|
+
Draw a filled circle and optional outline using mvcrender.draw.circle_u8.
|
307
|
+
If alpha<255, preblend once at the center and stamp solid.
|
356
308
|
"""
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
#
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
max_x = min(width, x + radius + outline_width + 1)
|
365
|
-
|
366
|
-
# Create coordinate arrays for the bounding box
|
367
|
-
y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
|
368
|
-
|
369
|
-
# Calculate distances from center
|
370
|
-
dist_sq = (y_indices - y) ** 2 + (x_indices - x) ** 2
|
309
|
+
cy, cx = (
|
310
|
+
int(center[0]),
|
311
|
+
int(center[1]),
|
312
|
+
) # incoming Point is (y,x) in your codebase
|
313
|
+
h, w = image.shape[:2]
|
314
|
+
if not (0 <= cx < w and 0 <= cy < h):
|
315
|
+
return image
|
371
316
|
|
372
|
-
|
373
|
-
|
317
|
+
fill_rgba = color
|
318
|
+
if fill_rgba[3] < 255:
|
319
|
+
fill_rgba = sample_and_blend_color(image, cx, cy, fill_rgba)
|
374
320
|
|
375
|
-
|
376
|
-
image[min_y:max_y, min_x:max_x][circle_mask] = color
|
321
|
+
circle_u8(image, int(cx), int(cy), int(radius), fill_rgba, -1)
|
377
322
|
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
323
|
+
if outline_color is not None and outline_width > 0:
|
324
|
+
out_rgba = outline_color
|
325
|
+
if out_rgba[3] < 255:
|
326
|
+
out_rgba = sample_and_blend_color(image, cx, cy, out_rgba)
|
327
|
+
# outlined stroke thickness = outline_width
|
328
|
+
circle_u8(
|
329
|
+
image, int(cx), int(cy), int(radius), out_rgba, int(outline_width)
|
330
|
+
)
|
383
331
|
|
384
332
|
return image
|
385
333
|
|
@@ -466,9 +414,6 @@ class Drawable:
|
|
466
414
|
# Adjust points to the mask's coordinate system
|
467
415
|
adjusted_points = [(p[0] - min_x, p[1] - min_y) for p in points]
|
468
416
|
|
469
|
-
# Create a grid of coordinates and use it to test all points at once
|
470
|
-
y_indices, x_indices = np.mgrid[0 : mask.shape[0], 0 : mask.shape[1]]
|
471
|
-
|
472
417
|
# Test each point in the grid
|
473
418
|
for i in range(mask.shape[0]):
|
474
419
|
for j in range(mask.shape[1]):
|
@@ -545,68 +490,63 @@ class Drawable:
|
|
545
490
|
angle: float,
|
546
491
|
fill: Color,
|
547
492
|
robot_state: str | None = None,
|
493
|
+
radius: int = 25, # user-configurable
|
548
494
|
) -> NumpyArray:
|
549
495
|
"""
|
550
|
-
Draw the robot
|
551
|
-
Optimized with NumPy vectorized operations for better performance.
|
496
|
+
Draw the robot with configurable size. All elements scale with radius.
|
552
497
|
"""
|
553
|
-
#
|
498
|
+
# Minimum radius to keep things visible
|
499
|
+
radius = max(8, min(radius, 25))
|
500
|
+
|
554
501
|
height, width = layers.shape[:2]
|
555
502
|
if not (0 <= x < width and 0 <= y < height):
|
556
503
|
return layers
|
557
504
|
|
558
|
-
#
|
559
|
-
radius = 25
|
560
|
-
box_size = radius * 2 + 2 # Add a small margin
|
561
|
-
|
562
|
-
# Calculate the region to draw on
|
505
|
+
# Bounding box
|
563
506
|
top_left_x = max(0, x - radius - 1)
|
564
507
|
top_left_y = max(0, y - radius - 1)
|
565
508
|
bottom_right_x = min(width, x + radius + 1)
|
566
509
|
bottom_right_y = min(height, y + radius + 1)
|
567
510
|
|
568
|
-
# Skip if the robot is completely outside the image
|
569
511
|
if top_left_x >= bottom_right_x or top_left_y >= bottom_right_y:
|
570
512
|
return layers
|
571
513
|
|
572
|
-
# Create a temporary layer for the robot
|
573
514
|
tmp_width = bottom_right_x - top_left_x
|
574
515
|
tmp_height = bottom_right_y - top_left_y
|
575
516
|
tmp_layer = layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x].copy()
|
576
517
|
|
577
|
-
# Calculate the robot center in the temporary layer
|
578
518
|
tmp_x = x - top_left_x
|
579
519
|
tmp_y = y - top_left_y
|
580
520
|
|
581
|
-
#
|
582
|
-
r_scaled = radius
|
583
|
-
r_cover = r_scaled *
|
521
|
+
# All geometry proportional to radius
|
522
|
+
r_scaled: float = max(1.0, radius / 11.0)
|
523
|
+
r_cover = int(r_scaled * 10)
|
524
|
+
r_lidar = max(1, int(r_scaled * 3))
|
525
|
+
r_button = max(1, int(r_scaled * 1))
|
526
|
+
lidar_offset = int(radius * 0.6) # was fixed 15
|
527
|
+
button_offset = int(radius * 0.8) # was fixed 20
|
528
|
+
|
584
529
|
lidar_angle = np.deg2rad(angle + 90)
|
585
|
-
r_lidar = r_scaled * 3
|
586
|
-
r_button = r_scaled * 1
|
587
530
|
|
588
|
-
# Set colors based on robot state
|
589
531
|
if robot_state == "error":
|
590
532
|
outline = Drawable.ERROR_OUTLINE
|
591
533
|
fill = Drawable.ERROR_COLOR
|
592
534
|
else:
|
593
535
|
outline = (fill[0] // 2, fill[1] // 2, fill[2] // 2, fill[3])
|
594
536
|
|
595
|
-
#
|
537
|
+
# Body
|
596
538
|
tmp_layer = Drawable._filled_circle(
|
597
539
|
tmp_layer, (tmp_y, tmp_x), radius, fill, outline, 1
|
598
540
|
)
|
599
541
|
|
600
|
-
#
|
542
|
+
# Direction wedge
|
601
543
|
angle -= 90
|
602
|
-
a1 = ((angle + 90) - 80)
|
603
|
-
a2 = ((angle + 90) + 80)
|
604
|
-
x1 = int(tmp_x - r_cover *
|
605
|
-
y1 = int(tmp_y + r_cover *
|
606
|
-
x2 = int(tmp_x - r_cover *
|
607
|
-
y2 = int(tmp_y + r_cover *
|
608
|
-
|
609
|
-
# Draw the direction line
|
544
|
+
a1 = np.deg2rad((angle + 90) - 80)
|
545
|
+
a2 = np.deg2rad((angle + 90) + 80)
|
546
|
+
x1 = int(tmp_x - r_cover * np.sin(a1))
|
547
|
+
y1 = int(tmp_y + r_cover * np.cos(a1))
|
548
|
+
x2 = int(tmp_x - r_cover * np.sin(a2))
|
549
|
+
y2 = int(tmp_y + r_cover * np.cos(a2))
|
610
550
|
if (
|
611
551
|
0 <= x1 < tmp_width
|
612
552
|
and 0 <= y1 < tmp_height
|
@@ -615,25 +555,23 @@ class Drawable:
|
|
615
555
|
):
|
616
556
|
tmp_layer = Drawable._line(tmp_layer, x1, y1, x2, y2, outline, width=1)
|
617
557
|
|
618
|
-
#
|
619
|
-
lidar_x = int(tmp_x +
|
620
|
-
lidar_y = int(tmp_y +
|
558
|
+
# Lidar
|
559
|
+
lidar_x = int(tmp_x + lidar_offset * np.cos(lidar_angle))
|
560
|
+
lidar_y = int(tmp_y + lidar_offset * np.sin(lidar_angle))
|
621
561
|
if 0 <= lidar_x < tmp_width and 0 <= lidar_y < tmp_height:
|
622
562
|
tmp_layer = Drawable._filled_circle(
|
623
563
|
tmp_layer, (lidar_y, lidar_x), r_lidar, outline
|
624
564
|
)
|
625
565
|
|
626
|
-
#
|
627
|
-
butt_x = int(tmp_x -
|
628
|
-
butt_y = int(tmp_y -
|
566
|
+
# Button
|
567
|
+
butt_x = int(tmp_x - button_offset * np.cos(lidar_angle))
|
568
|
+
butt_y = int(tmp_y - button_offset * np.sin(lidar_angle))
|
629
569
|
if 0 <= butt_x < tmp_width and 0 <= butt_y < tmp_height:
|
630
570
|
tmp_layer = Drawable._filled_circle(
|
631
571
|
tmp_layer, (butt_y, butt_x), r_button, outline
|
632
572
|
)
|
633
573
|
|
634
|
-
# Copy the robot layer back to the main layer
|
635
574
|
layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x] = tmp_layer
|
636
|
-
|
637
575
|
return layers
|
638
576
|
|
639
577
|
@staticmethod
|
@@ -781,11 +719,11 @@ class Drawable:
|
|
781
719
|
continue
|
782
720
|
|
783
721
|
t = np.linspace(0, 1, length * 2)
|
784
|
-
|
785
|
-
|
722
|
+
x_coordinates = np.round(x1 * (1 - t) + x2 * t).astype(int)
|
723
|
+
y_coordinates = np.round(y1 * (1 - t) + y2 * t).astype(int)
|
786
724
|
|
787
725
|
# Add line points to mask
|
788
|
-
for x, y in zip(
|
726
|
+
for x, y in zip(x_coordinates, y_coordinates):
|
789
727
|
if width == 1:
|
790
728
|
mask[y, x] = True
|
791
729
|
else:
|
@@ -827,7 +765,6 @@ class Drawable:
|
|
827
765
|
|
828
766
|
# Precompute circular mask for radius
|
829
767
|
radius = 6
|
830
|
-
diameter = radius * 2 + 1
|
831
768
|
yy, xx = np.ogrid[-radius : radius + 1, -radius : radius + 1]
|
832
769
|
circle_mask = (xx**2 + yy**2) <= radius**2
|
833
770
|
|
@@ -842,9 +779,7 @@ class Drawable:
|
|
842
779
|
continue
|
843
780
|
|
844
781
|
if need_blending:
|
845
|
-
obs_color =
|
846
|
-
image, x, y, color
|
847
|
-
)
|
782
|
+
obs_color = sample_and_blend_color(image, x, y, color)
|
848
783
|
else:
|
849
784
|
obs_color = color
|
850
785
|
|
@@ -882,11 +817,25 @@ class Drawable:
|
|
882
817
|
position: bool,
|
883
818
|
) -> None:
|
884
819
|
"""Draw the status text on the image."""
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
820
|
+
module_dir = Path(__file__).resolve().parent
|
821
|
+
default_font_path = module_dir / "fonts" / "FiraSans.ttf"
|
822
|
+
# Load default font with safety fallback to PIL's built-in if missing
|
823
|
+
try:
|
824
|
+
default_font = ImageFont.truetype(str(default_font_path), size)
|
825
|
+
except OSError:
|
826
|
+
_LOGGER.warning(
|
827
|
+
"Default font not found at %s; using PIL default font",
|
828
|
+
default_font_path,
|
829
|
+
)
|
830
|
+
default_font = ImageFont.load_default()
|
831
|
+
|
832
|
+
# Use provided font directly if available; else fall back to default
|
833
|
+
user_font = default_font
|
834
|
+
if path_font:
|
835
|
+
try:
|
836
|
+
user_font = ImageFont.truetype(str(path_font), size)
|
837
|
+
except OSError:
|
838
|
+
user_font = default_font
|
890
839
|
if position:
|
891
840
|
x, y = 10, 10
|
892
841
|
else:
|