valetudo-map-parser 0.1.10rc7__py3-none-any.whl → 0.1.11b0__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 +8 -10
- valetudo_map_parser/config/drawable.py +91 -329
- valetudo_map_parser/config/drawable_elements.py +0 -2
- valetudo_map_parser/config/rand256_parser.py +51 -15
- valetudo_map_parser/config/shared.py +25 -6
- valetudo_map_parser/config/status_text/status_text.py +1 -0
- valetudo_map_parser/config/types.py +5 -4
- valetudo_map_parser/config/utils.py +32 -130
- valetudo_map_parser/hypfer_draw.py +0 -2
- valetudo_map_parser/hypfer_handler.py +13 -19
- valetudo_map_parser/map_data.py +15 -10
- valetudo_map_parser/rand256_handler.py +68 -38
- valetudo_map_parser/reimg_draw.py +1 -1
- valetudo_map_parser/rooms_handler.py +10 -10
- {valetudo_map_parser-0.1.10rc7.dist-info → valetudo_map_parser-0.1.11b0.dist-info}/METADATA +2 -2
- valetudo_map_parser-0.1.11b0.dist-info/RECORD +32 -0
- valetudo_map_parser/config/auto_crop.py +0 -452
- valetudo_map_parser/config/color_utils.py +0 -105
- valetudo_map_parser/config/enhanced_drawable.py +0 -324
- valetudo_map_parser-0.1.10rc7.dist-info/RECORD +0 -35
- {valetudo_map_parser-0.1.10rc7.dist-info → valetudo_map_parser-0.1.11b0.dist-info}/WHEEL +0 -0
- {valetudo_map_parser-0.1.10rc7.dist-info → valetudo_map_parser-0.1.11b0.dist-info}/licenses/LICENSE +0 -0
- {valetudo_map_parser-0.1.10rc7.dist-info → valetudo_map_parser-0.1.11b0.dist-info}/licenses/NOTICE.txt +0 -0
@@ -1,7 +1,8 @@
|
|
1
|
-
"""New Rand256 Map Parser -
|
1
|
+
"""New Rand256 Map Parser -
|
2
|
+
Based on Xiaomi/Roborock implementation with precise binary parsing."""
|
2
3
|
|
3
|
-
import struct
|
4
4
|
import math
|
5
|
+
import struct
|
5
6
|
from enum import Enum
|
6
7
|
from typing import Any, Dict, List, Optional
|
7
8
|
|
@@ -78,6 +79,7 @@ class RRMapParser:
|
|
78
79
|
|
79
80
|
@staticmethod
|
80
81
|
def _parse_carpet_map(data: bytes) -> set[int]:
|
82
|
+
"""Parse carpet map using Xiaomi method."""
|
81
83
|
carpet_map = set()
|
82
84
|
|
83
85
|
for i, v in enumerate(data):
|
@@ -87,6 +89,7 @@ class RRMapParser:
|
|
87
89
|
|
88
90
|
@staticmethod
|
89
91
|
def _parse_area(header: bytes, data: bytes) -> list:
|
92
|
+
"""Parse area using Xiaomi method."""
|
90
93
|
area_pairs = RRMapParser._get_int16(header, 0x08)
|
91
94
|
areas = []
|
92
95
|
for area_start in range(0, area_pairs * 16, 16):
|
@@ -114,6 +117,7 @@ class RRMapParser:
|
|
114
117
|
|
115
118
|
@staticmethod
|
116
119
|
def _parse_zones(data: bytes, header: bytes) -> list:
|
120
|
+
"""Parse zones using Xiaomi method."""
|
117
121
|
zone_pairs = RRMapParser._get_int16(header, 0x08)
|
118
122
|
zones = []
|
119
123
|
for zone_start in range(0, zone_pairs * 8, 8):
|
@@ -146,9 +150,9 @@ class RRMapParser:
|
|
146
150
|
angle = raw_angle
|
147
151
|
return {"position": [x, y], "angle": angle}
|
148
152
|
|
149
|
-
|
150
153
|
@staticmethod
|
151
154
|
def _parse_walls(data: bytes, header: bytes) -> list:
|
155
|
+
"""Parse walls using Xiaomi method."""
|
152
156
|
wall_pairs = RRMapParser._get_int16(header, 0x08)
|
153
157
|
walls = []
|
154
158
|
for wall_start in range(0, wall_pairs * 8, 8):
|
@@ -156,7 +160,14 @@ class RRMapParser:
|
|
156
160
|
y0 = RRMapParser._get_int16(data, wall_start + 2)
|
157
161
|
x1 = RRMapParser._get_int16(data, wall_start + 4)
|
158
162
|
y1 = RRMapParser._get_int16(data, wall_start + 6)
|
159
|
-
walls.append(
|
163
|
+
walls.append(
|
164
|
+
[
|
165
|
+
x0,
|
166
|
+
RRMapParser.Tools.DIMENSION_MM - y0,
|
167
|
+
x1,
|
168
|
+
RRMapParser.Tools.DIMENSION_MM - y1,
|
169
|
+
]
|
170
|
+
)
|
160
171
|
return walls
|
161
172
|
|
162
173
|
@staticmethod
|
@@ -204,6 +215,7 @@ class RRMapParser:
|
|
204
215
|
return {}
|
205
216
|
|
206
217
|
def parse_blocks(self, raw: bytes, pixels: bool = True) -> Dict[int, Any]:
|
218
|
+
"""Parse all blocks using Xiaomi method."""
|
207
219
|
blocks = {}
|
208
220
|
map_header_length = self._get_int16(raw, 0x02)
|
209
221
|
block_start_position = map_header_length
|
@@ -218,29 +230,53 @@ class RRMapParser:
|
|
218
230
|
match block_type:
|
219
231
|
case self.Types.DIGEST.value:
|
220
232
|
self.is_valid = True
|
221
|
-
case
|
222
|
-
|
233
|
+
case (
|
234
|
+
self.Types.ROBOT_POSITION.value
|
235
|
+
| self.Types.CHARGER_LOCATION.value
|
236
|
+
):
|
237
|
+
blocks[block_type] = self._parse_object_position(
|
238
|
+
block_data_length, data
|
239
|
+
)
|
223
240
|
case self.Types.PATH.value | self.Types.GOTO_PREDICTED_PATH.value:
|
224
|
-
blocks[block_type] = self._parse_path_block(
|
241
|
+
blocks[block_type] = self._parse_path_block(
|
242
|
+
raw, block_start_position, block_data_length
|
243
|
+
)
|
225
244
|
case self.Types.CURRENTLY_CLEANED_ZONES.value:
|
226
245
|
blocks[block_type] = {"zones": self._parse_zones(data, header)}
|
227
246
|
case self.Types.FORBIDDEN_ZONES.value:
|
228
|
-
blocks[block_type] = {
|
247
|
+
blocks[block_type] = {
|
248
|
+
"forbidden_zones": self._parse_area(header, data)
|
249
|
+
}
|
229
250
|
case self.Types.FORBIDDEN_MOP_ZONES.value:
|
230
|
-
blocks[block_type] = {
|
251
|
+
blocks[block_type] = {
|
252
|
+
"forbidden_mop_zones": self._parse_area(header, data)
|
253
|
+
}
|
231
254
|
case self.Types.GOTO_TARGET.value:
|
232
255
|
blocks[block_type] = {"position": self._parse_goto_target(data)}
|
233
256
|
case self.Types.VIRTUAL_WALLS.value:
|
234
|
-
blocks[block_type] = {
|
257
|
+
blocks[block_type] = {
|
258
|
+
"virtual_walls": self._parse_walls(data, header)
|
259
|
+
}
|
235
260
|
case self.Types.CARPET_MAP.value:
|
236
|
-
data = RRMapParser._get_bytes(
|
237
|
-
|
261
|
+
data = RRMapParser._get_bytes(
|
262
|
+
raw, block_data_start, block_data_length
|
263
|
+
)
|
264
|
+
blocks[block_type] = {
|
265
|
+
"carpet_map": self._parse_carpet_map(data)
|
266
|
+
}
|
238
267
|
case self.Types.IMAGE.value:
|
239
268
|
header_length = self._get_int8(header, 2)
|
240
269
|
blocks[block_type] = self._parse_image_block(
|
241
|
-
raw,
|
242
|
-
|
243
|
-
|
270
|
+
raw,
|
271
|
+
block_start_position,
|
272
|
+
block_data_length,
|
273
|
+
header_length,
|
274
|
+
pixels,
|
275
|
+
)
|
276
|
+
|
277
|
+
block_start_position = (
|
278
|
+
block_start_position + block_data_length + self._get_int8(header, 2)
|
279
|
+
)
|
244
280
|
except (struct.error, IndexError):
|
245
281
|
break
|
246
282
|
return blocks
|
@@ -7,18 +7,20 @@ Version: v0.1.12
|
|
7
7
|
import asyncio
|
8
8
|
import logging
|
9
9
|
from typing import List
|
10
|
+
|
10
11
|
from PIL import Image
|
11
12
|
|
13
|
+
from .utils import pil_size_rotation
|
12
14
|
from .types import (
|
13
15
|
ATTR_CALIBRATION_POINTS,
|
14
16
|
ATTR_CAMERA_MODE,
|
15
17
|
ATTR_CONTENT_TYPE,
|
18
|
+
ATTR_IMAGE_LAST_UPDATED,
|
16
19
|
ATTR_MARGINS,
|
17
20
|
ATTR_OBSTACLES,
|
18
21
|
ATTR_POINTS,
|
19
22
|
ATTR_ROOMS,
|
20
23
|
ATTR_ROTATE,
|
21
|
-
ATTR_IMAGE_LAST_UPDATED,
|
22
24
|
ATTR_VACUUM_BATTERY,
|
23
25
|
ATTR_VACUUM_CHARGING,
|
24
26
|
ATTR_VACUUM_JSON_ID,
|
@@ -38,10 +40,11 @@ from .types import (
|
|
38
40
|
CONF_VAC_STAT_SIZE,
|
39
41
|
CONF_ZOOM_LOCK_RATIO,
|
40
42
|
DEFAULT_VALUES,
|
43
|
+
NOT_STREAMING_STATES,
|
41
44
|
CameraModes,
|
42
45
|
Colors,
|
43
|
-
TrimsData,
|
44
46
|
PilPNG,
|
47
|
+
TrimsData,
|
45
48
|
)
|
46
49
|
|
47
50
|
|
@@ -117,10 +120,17 @@ class CameraShared:
|
|
117
120
|
self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"])
|
118
121
|
self.skip_room_ids: List[str] = []
|
119
122
|
self.device_info = None
|
123
|
+
self._battery_state = None
|
120
124
|
|
121
125
|
def vacuum_bat_charged(self) -> bool:
|
122
126
|
"""Check if the vacuum is charging."""
|
123
|
-
|
127
|
+
if self.vacuum_state != "docked":
|
128
|
+
self._battery_state = "not_charging"
|
129
|
+
elif (self._battery_state == "charging") and (int(self.vacuum_battery) == 100):
|
130
|
+
self._battery_state = "charged"
|
131
|
+
else:
|
132
|
+
self._battery_state = "charging" if int(self.vacuum_battery) < 100 else "not_charging"
|
133
|
+
return (self.vacuum_state == "docked") and (self._battery_state == "charged")
|
124
134
|
|
125
135
|
@staticmethod
|
126
136
|
def _compose_obstacle_links(vacuum_host_ip: str, obstacles: list) -> list | None:
|
@@ -207,16 +217,25 @@ class CameraShared:
|
|
207
217
|
|
208
218
|
return attrs
|
209
219
|
|
220
|
+
def is_streaming(self) -> bool:
|
221
|
+
"""Return true if the device is streaming."""
|
222
|
+
updated_status = self.vacuum_state
|
223
|
+
attr_is_streaming = ((updated_status not in NOT_STREAMING_STATES
|
224
|
+
or self.vacuum_bat_charged())
|
225
|
+
or not self.binary_image)
|
226
|
+
return attr_is_streaming
|
227
|
+
|
210
228
|
def to_dict(self) -> dict:
|
211
229
|
"""Return a dictionary with image and attributes data."""
|
212
|
-
|
230
|
+
data = {
|
213
231
|
"image": {
|
214
232
|
"binary": self.binary_image,
|
215
|
-
"
|
216
|
-
"
|
233
|
+
"size": pil_size_rotation(self.image_rotate, self.new_image),
|
234
|
+
"streaming": self.is_streaming()
|
217
235
|
},
|
218
236
|
"attributes": self.generate_attributes(),
|
219
237
|
}
|
238
|
+
return data
|
220
239
|
|
221
240
|
|
222
241
|
class CameraSharedManager:
|
@@ -8,7 +8,7 @@ import json
|
|
8
8
|
import logging
|
9
9
|
import threading
|
10
10
|
from dataclasses import asdict, dataclass
|
11
|
-
from typing import Any, Dict, Optional, Tuple, TypedDict, Union
|
11
|
+
from typing import Any, Dict, List, NotRequired, Optional, Tuple, TypedDict, Union
|
12
12
|
|
13
13
|
import numpy as np
|
14
14
|
from PIL import Image
|
@@ -34,12 +34,11 @@ class Room(TypedDict):
|
|
34
34
|
id: int
|
35
35
|
|
36
36
|
|
37
|
-
# list[dict[str, str | list[int]]] | list[dict[str, str | list[list[int]]]] | list[dict[str, str | int]] | int]'
|
38
37
|
class Destinations(TypedDict, total=False):
|
39
38
|
spots: NotRequired[Optional[List[Spot]]]
|
40
39
|
zones: NotRequired[Optional[List[Zone]]]
|
41
40
|
rooms: NotRequired[Optional[List[Room]]]
|
42
|
-
updated: NotRequired[Optional[float]]
|
41
|
+
updated: NotRequired[Optional[float | int]]
|
43
42
|
|
44
43
|
|
45
44
|
class RoomProperty(TypedDict):
|
@@ -222,7 +221,9 @@ class SnapshotStore:
|
|
222
221
|
Color = Union[Tuple[int, int, int], Tuple[int, int, int, int]]
|
223
222
|
Colors = Dict[str, Color]
|
224
223
|
CalibrationPoints = list[dict[str, Any]]
|
225
|
-
RobotPosition
|
224
|
+
RobotPosition: type[tuple[Any, Any, dict[str, int | float] | None]] = tuple[
|
225
|
+
Any, Any, dict[str, int | float] | None
|
226
|
+
]
|
226
227
|
ChargerPosition = dict[str, Any]
|
227
228
|
RoomsProperties = dict[str, RoomProperty]
|
228
229
|
ImageSize = dict[str, int | list[int]]
|
@@ -1,32 +1,30 @@
|
|
1
1
|
"""Utility code for the valetudo map parser."""
|
2
2
|
|
3
3
|
import datetime
|
4
|
-
from time import time
|
5
4
|
import hashlib
|
5
|
+
import io
|
6
6
|
import json
|
7
7
|
from dataclasses import dataclass
|
8
|
+
from time import time
|
8
9
|
from typing import Callable, List, Optional, Tuple
|
9
|
-
import io
|
10
10
|
|
11
11
|
import numpy as np
|
12
12
|
from PIL import Image, ImageOps
|
13
13
|
|
14
|
+
from ..map_data import HyperMapData
|
15
|
+
from .async_utils import AsyncNumPy
|
14
16
|
from .drawable import Drawable
|
15
17
|
from .drawable_elements import DrawingConfig
|
16
|
-
from .enhanced_drawable import EnhancedDrawable
|
17
18
|
from .status_text.status_text import StatusText
|
18
|
-
|
19
19
|
from .types import (
|
20
20
|
LOGGER,
|
21
21
|
ChargerPosition,
|
22
|
-
|
22
|
+
Destinations,
|
23
23
|
NumpyArray,
|
24
24
|
PilPNG,
|
25
25
|
RobotPosition,
|
26
|
-
|
26
|
+
Size,
|
27
27
|
)
|
28
|
-
from ..map_data import HyperMapData
|
29
|
-
from .async_utils import AsyncNumPy
|
30
28
|
|
31
29
|
|
32
30
|
@dataclass
|
@@ -79,7 +77,6 @@ class BaseHandler:
|
|
79
77
|
# Drawing components are initialized by initialize_drawing_config in handlers
|
80
78
|
self.drawing_config: Optional[DrawingConfig] = None
|
81
79
|
self.draw: Optional[Drawable] = None
|
82
|
-
self.enhanced_draw: Optional[EnhancedDrawable] = None
|
83
80
|
|
84
81
|
def get_frame_number(self) -> int:
|
85
82
|
"""Return the frame number of the image."""
|
@@ -114,6 +111,12 @@ class BaseHandler:
|
|
114
111
|
try:
|
115
112
|
# Backup current image to last_image before processing new one
|
116
113
|
if hasattr(self.shared, "new_image") and self.shared.new_image is not None:
|
114
|
+
# Close old last_image to free memory before replacing it
|
115
|
+
if hasattr(self.shared, "last_image") and self.shared.last_image is not None:
|
116
|
+
try:
|
117
|
+
self.shared.last_image.close()
|
118
|
+
except Exception:
|
119
|
+
pass # Ignore errors if image is already closed
|
117
120
|
self.shared.last_image = self.shared.new_image
|
118
121
|
|
119
122
|
# Call the appropriate handler method based on handler type
|
@@ -174,13 +177,11 @@ class BaseHandler:
|
|
174
177
|
LOGGER.warning(
|
175
178
|
"%s: Failed to generate image from JSON data", self.file_name
|
176
179
|
)
|
177
|
-
if bytes_format and hasattr(self.shared, "last_image"):
|
178
|
-
return pil_to_png_bytes(self.shared.last_image), {}
|
179
180
|
return (
|
180
181
|
self.shared.last_image
|
181
182
|
if hasattr(self.shared, "last_image")
|
182
183
|
else None
|
183
|
-
),
|
184
|
+
), self.shared.to_dict()
|
184
185
|
|
185
186
|
except Exception as e:
|
186
187
|
LOGGER.warning(
|
@@ -199,10 +200,11 @@ class BaseHandler:
|
|
199
200
|
if hasattr(self, "get_rooms_attributes") and (
|
200
201
|
self.shared.map_rooms is None and destinations is not None
|
201
202
|
):
|
202
|
-
|
203
|
+
self.shared.map_rooms = await self.get_rooms_attributes(destinations)
|
203
204
|
if self.shared.map_rooms:
|
204
205
|
LOGGER.debug("%s: Rand256 attributes rooms updated", self.file_name)
|
205
206
|
|
207
|
+
|
206
208
|
if hasattr(self, "async_get_rooms_attributes") and (
|
207
209
|
self.shared.map_rooms is None
|
208
210
|
):
|
@@ -233,15 +235,8 @@ class BaseHandler:
|
|
233
235
|
self, pil_img: PilPNG, rand: bool = False
|
234
236
|
) -> ResizeParams:
|
235
237
|
"""Prepare resize parameters for image resizing."""
|
236
|
-
|
237
|
-
|
238
|
-
else:
|
239
|
-
height, width = pil_img.size
|
240
|
-
LOGGER.debug(
|
241
|
-
"Shared PIL image size: %s x %s",
|
242
|
-
self.shared.image_ref_width,
|
243
|
-
self.shared.image_ref_height,
|
244
|
-
)
|
238
|
+
width, height = pil_size_rotation(self.shared.image_rotate, pil_img)
|
239
|
+
|
245
240
|
return ResizeParams(
|
246
241
|
pil_img=pil_img,
|
247
242
|
width=width,
|
@@ -662,9 +657,6 @@ class BaseHandler:
|
|
662
657
|
|
663
658
|
async def async_resize_image(params: ResizeParams):
|
664
659
|
"""Resize the image to the given dimensions and aspect ratio."""
|
665
|
-
LOGGER.debug("Resizing image to aspect ratio: %s", params.aspect_ratio)
|
666
|
-
LOGGER.debug("Original image size: %s x %s", params.width, params.height)
|
667
|
-
LOGGER.debug("Image crop size: %s", params.crop_size)
|
668
660
|
if params.aspect_ratio == "None":
|
669
661
|
return params.pil_img
|
670
662
|
if params.aspect_ratio != "None":
|
@@ -701,6 +693,17 @@ async def async_resize_image(params: ResizeParams):
|
|
701
693
|
return params.pil_img
|
702
694
|
|
703
695
|
|
696
|
+
def pil_size_rotation(image_rotate, pil_img):
|
697
|
+
"""Return the size of the image."""
|
698
|
+
if not pil_img:
|
699
|
+
return 0, 0
|
700
|
+
if image_rotate in [0, 180]:
|
701
|
+
width, height = pil_img.size
|
702
|
+
else:
|
703
|
+
height, width = pil_img.size
|
704
|
+
return width, height
|
705
|
+
|
706
|
+
|
704
707
|
def initialize_drawing_config(handler):
|
705
708
|
"""
|
706
709
|
Initialize drawing configuration from device_info.
|
@@ -709,7 +712,7 @@ def initialize_drawing_config(handler):
|
|
709
712
|
handler: The handler instance with shared data and file_name attributes
|
710
713
|
|
711
714
|
Returns:
|
712
|
-
Tuple of (DrawingConfig, Drawable
|
715
|
+
Tuple of (DrawingConfig, Drawable)
|
713
716
|
"""
|
714
717
|
|
715
718
|
# Initialize drawing configuration
|
@@ -721,98 +724,10 @@ def initialize_drawing_config(handler):
|
|
721
724
|
):
|
722
725
|
drawing_config.update_from_device_info(handler.shared.device_info)
|
723
726
|
|
724
|
-
# Initialize
|
725
|
-
draw = Drawable()
|
726
|
-
enhanced_draw = EnhancedDrawable(drawing_config) # New enhanced drawing system
|
727
|
+
# Initialize drawing utilities
|
728
|
+
draw = Drawable()
|
727
729
|
|
728
|
-
return drawing_config, draw
|
729
|
-
|
730
|
-
|
731
|
-
def blend_colors(base_color, overlay_color):
|
732
|
-
"""
|
733
|
-
Blend two RGBA colors using alpha compositing.
|
734
|
-
|
735
|
-
Args:
|
736
|
-
base_color: Base RGBA color tuple (r, g, b, a)
|
737
|
-
overlay_color: Overlay RGBA color tuple (r, g, b, a)
|
738
|
-
|
739
|
-
Returns:
|
740
|
-
Blended RGBA color tuple (r, g, b, a)
|
741
|
-
"""
|
742
|
-
r1, g1, b1, a1 = base_color
|
743
|
-
r2, g2, b2, a2 = overlay_color
|
744
|
-
|
745
|
-
# Convert alpha to 0-1 range
|
746
|
-
a1 = a1 / 255.0
|
747
|
-
a2 = a2 / 255.0
|
748
|
-
|
749
|
-
# Calculate resulting alpha
|
750
|
-
a_out = a1 + a2 * (1 - a1)
|
751
|
-
|
752
|
-
# Avoid division by zero
|
753
|
-
if a_out < 0.0001:
|
754
|
-
return [0, 0, 0, 0]
|
755
|
-
|
756
|
-
# Calculate blended RGB components
|
757
|
-
r_out = (r1 * a1 + r2 * a2 * (1 - a1)) / a_out
|
758
|
-
g_out = (g1 * a1 + g2 * a2 * (1 - a1)) / a_out
|
759
|
-
b_out = (b1 * a1 + b2 * a2 * (1 - a1)) / a_out
|
760
|
-
|
761
|
-
# Convert back to 0-255 range and return as tuple
|
762
|
-
return (
|
763
|
-
int(max(0, min(255, r_out))),
|
764
|
-
int(max(0, min(255, g_out))),
|
765
|
-
int(max(0, min(255, b_out))),
|
766
|
-
int(max(0, min(255, a_out * 255))),
|
767
|
-
)
|
768
|
-
|
769
|
-
|
770
|
-
def blend_pixel(array, x, y, color, element, element_map=None, drawing_config=None):
|
771
|
-
"""
|
772
|
-
Blend a pixel color with the existing color at the specified position.
|
773
|
-
Also updates the element map if the new element has higher z-index.
|
774
|
-
|
775
|
-
Args:
|
776
|
-
array: The image array to modify
|
777
|
-
x: X coordinate
|
778
|
-
y: Y coordinate
|
779
|
-
color: RGBA color tuple to blend
|
780
|
-
element: Element code for the pixel
|
781
|
-
element_map: Optional element map to update
|
782
|
-
drawing_config: Optional drawing configuration for z-index lookup
|
783
|
-
|
784
|
-
Returns:
|
785
|
-
None
|
786
|
-
"""
|
787
|
-
# Check bounds
|
788
|
-
if not (0 <= y < array.shape[0] and 0 <= x < array.shape[1]):
|
789
|
-
return
|
790
|
-
|
791
|
-
# Get current element at this position
|
792
|
-
current_element = None
|
793
|
-
if element_map is not None:
|
794
|
-
current_element = element_map[y, x]
|
795
|
-
|
796
|
-
# Get z-index values for comparison
|
797
|
-
current_z = 0
|
798
|
-
new_z = 0
|
799
|
-
|
800
|
-
if drawing_config is not None:
|
801
|
-
current_z = (
|
802
|
-
drawing_config.get_property(current_element, "z_index", 0)
|
803
|
-
if current_element
|
804
|
-
else 0
|
805
|
-
)
|
806
|
-
new_z = drawing_config.get_property(element, "z_index", 0)
|
807
|
-
|
808
|
-
# Update element map if new element has higher z-index
|
809
|
-
if element_map is not None and new_z >= current_z:
|
810
|
-
element_map[y, x] = element
|
811
|
-
|
812
|
-
# Blend colors
|
813
|
-
base_color = array[y, x]
|
814
|
-
blended_color = blend_colors(base_color, color)
|
815
|
-
array[y, x] = blended_color
|
730
|
+
return drawing_config, draw
|
816
731
|
|
817
732
|
|
818
733
|
def manage_drawable_elements(
|
@@ -996,12 +911,6 @@ async def async_extract_room_outline(
|
|
996
911
|
|
997
912
|
# If we found too few boundary points, use the rectangular outline
|
998
913
|
if len(boundary_points) < 8: # Need at least 8 points for a meaningful shape
|
999
|
-
LOGGER.debug(
|
1000
|
-
"%s: Room %s has too few boundary points (%d), using rectangular outline",
|
1001
|
-
file_name,
|
1002
|
-
str(room_id_int),
|
1003
|
-
len(boundary_points),
|
1004
|
-
)
|
1005
914
|
return rect_outline
|
1006
915
|
|
1007
916
|
# Use a more sophisticated algorithm to create a coherent outline
|
@@ -1037,13 +946,6 @@ async def async_extract_room_outline(
|
|
1037
946
|
# Convert NumPy int64 values to regular Python integers
|
1038
947
|
simplified_outline = [(int(x), int(y)) for x, y in simplified_outline]
|
1039
948
|
|
1040
|
-
LOGGER.debug(
|
1041
|
-
"%s: Room %s outline has %d points",
|
1042
|
-
file_name,
|
1043
|
-
str(room_id_int),
|
1044
|
-
len(simplified_outline),
|
1045
|
-
)
|
1046
|
-
|
1047
949
|
return simplified_outline
|
1048
950
|
|
1049
951
|
except (ValueError, IndexError, TypeError, ArithmeticError) as e:
|
@@ -269,8 +269,6 @@ class ImageDraw:
|
|
269
269
|
zone_clean = self.img_h.data.find_zone_entities(m_json)
|
270
270
|
except (ValueError, KeyError):
|
271
271
|
zone_clean = None
|
272
|
-
else:
|
273
|
-
_LOGGER.info("%s: Got zones.", self.file_name)
|
274
272
|
|
275
273
|
if zone_clean:
|
276
274
|
# Process zones sequentially to avoid memory-intensive array copies
|
@@ -8,24 +8,22 @@ Version: 0.1.10
|
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
10
|
import asyncio
|
11
|
-
import numpy as np
|
12
11
|
|
12
|
+
import numpy as np
|
13
|
+
from mvcrender.autocrop import AutoCrop
|
13
14
|
from PIL import Image
|
14
15
|
|
15
16
|
from .config.async_utils import AsyncPIL
|
16
|
-
|
17
|
-
from mvcrender.autocrop import AutoCrop
|
18
17
|
from .config.drawable_elements import DrawableElement
|
19
18
|
from .config.shared import CameraShared
|
20
|
-
|
21
19
|
from .config.types import (
|
22
20
|
COLORS,
|
23
21
|
LOGGER,
|
24
22
|
CalibrationPoints,
|
25
23
|
Colors,
|
24
|
+
JsonType,
|
26
25
|
RoomsProperties,
|
27
26
|
RoomStore,
|
28
|
-
JsonType,
|
29
27
|
)
|
30
28
|
from .config.utils import (
|
31
29
|
BaseHandler,
|
@@ -48,9 +46,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
48
46
|
self.calibration_data = None # camera shared data.
|
49
47
|
self.data = ImageData # imported Image Data Module.
|
50
48
|
# Initialize drawing configuration using the shared utility function
|
51
|
-
self.drawing_config, self.draw
|
52
|
-
self
|
53
|
-
)
|
49
|
+
self.drawing_config, self.draw = initialize_drawing_config(self)
|
54
50
|
|
55
51
|
self.go_to = None # vacuum go to data
|
56
52
|
self.img_hash = None # hash of the image calculated to check differences.
|
@@ -77,7 +73,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
77
73
|
json_data
|
78
74
|
)
|
79
75
|
if room_properties:
|
80
|
-
|
76
|
+
_ = RoomStore(self.file_name, room_properties)
|
81
77
|
# Convert room_properties to the format expected by async_get_robot_in_room
|
82
78
|
self.rooms_pos = []
|
83
79
|
for room_id, room_data in room_properties.items():
|
@@ -258,7 +254,12 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
258
254
|
)
|
259
255
|
LOGGER.info("%s: Completed base Layers", self.file_name)
|
260
256
|
# Copy the new array in base layer.
|
257
|
+
# Delete old base layer before creating new one to free memory
|
258
|
+
if self.img_base_layer is not None:
|
259
|
+
del self.img_base_layer
|
261
260
|
self.img_base_layer = await self.async_copy_array(img_np_array)
|
261
|
+
# Delete source array after copying to free memory
|
262
|
+
del img_np_array
|
262
263
|
|
263
264
|
self.shared.frame_number = self.frame_number
|
264
265
|
self.frame_number += 1
|
@@ -272,6 +273,9 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
272
273
|
or self.img_work_layer.shape != self.img_base_layer.shape
|
273
274
|
or self.img_work_layer.dtype != self.img_base_layer.dtype
|
274
275
|
):
|
276
|
+
# Delete old buffer before creating new one to free memory
|
277
|
+
if self.img_work_layer is not None:
|
278
|
+
del self.img_work_layer
|
275
279
|
self.img_work_layer = np.empty_like(self.img_base_layer)
|
276
280
|
|
277
281
|
# Copy the base layer into the persistent working buffer (no new allocation per frame)
|
@@ -346,16 +350,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
346
350
|
robot_state=self.shared.vacuum_state,
|
347
351
|
)
|
348
352
|
|
349
|
-
# Update element map for robot position
|
350
|
-
if (
|
351
|
-
hasattr(self.shared, "element_map")
|
352
|
-
and self.shared.element_map is not None
|
353
|
-
):
|
354
|
-
update_element_map_with_robot(
|
355
|
-
self.shared.element_map,
|
356
|
-
robot_position,
|
357
|
-
DrawableElement.ROBOT,
|
358
|
-
)
|
359
353
|
# Synchronize zooming state from ImageDraw to handler before auto-crop
|
360
354
|
self.zooming = self.imd.img_h.zooming
|
361
355
|
|
valetudo_map_parser/map_data.py
CHANGED
@@ -8,22 +8,22 @@ Version: v0.1.10
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
-
import
|
11
|
+
from dataclasses import asdict, dataclass, field
|
12
12
|
from typing import (
|
13
|
-
List,
|
14
|
-
Sequence,
|
15
|
-
TypeVar,
|
16
13
|
Any,
|
17
|
-
TypedDict,
|
18
|
-
NotRequired,
|
19
14
|
Literal,
|
15
|
+
NotRequired,
|
20
16
|
Optional,
|
17
|
+
Sequence,
|
18
|
+
TypedDict,
|
19
|
+
TypeVar,
|
21
20
|
)
|
22
21
|
|
23
|
-
|
22
|
+
import numpy as np
|
24
23
|
|
25
24
|
from .config.types import ImageSize, JsonType
|
26
25
|
|
26
|
+
|
27
27
|
T = TypeVar("T")
|
28
28
|
|
29
29
|
# --- Common Nested Structures ---
|
@@ -373,6 +373,11 @@ class ImageData:
|
|
373
373
|
Else:
|
374
374
|
(min_x_mm, min_y_mm, max_x_mm, max_y_mm)
|
375
375
|
"""
|
376
|
+
|
377
|
+
def to_mm(coord):
|
378
|
+
"""Convert pixel coordinates to millimeters."""
|
379
|
+
return round(coord * pixel_size * 10)
|
380
|
+
|
376
381
|
if not pixels:
|
377
382
|
raise ValueError("Pixels list cannot be empty.")
|
378
383
|
|
@@ -393,7 +398,6 @@ class ImageData:
|
|
393
398
|
min_y = min(min_y, y)
|
394
399
|
|
395
400
|
if rand:
|
396
|
-
to_mm = lambda v: v * pixel_size * 10
|
397
401
|
return (to_mm(max_x), to_mm(max_y)), (to_mm(min_x), to_mm(min_y))
|
398
402
|
|
399
403
|
return (
|
@@ -548,8 +552,9 @@ class RandImageData:
|
|
548
552
|
@staticmethod
|
549
553
|
def get_rrm_forbidden_zones(json_data: JsonType) -> list[dict[str, Any]]:
|
550
554
|
"""Get the forbidden zones from the json."""
|
551
|
-
re_zones = json_data.get("forbidden_zones", [])
|
552
|
-
|
555
|
+
re_zones = json_data.get("forbidden_zones", []) + json_data.get(
|
556
|
+
"forbidden_mop_zones", []
|
557
|
+
)
|
553
558
|
formatted_zones = RandImageData._rrm_valetudo_format_zone(re_zones)
|
554
559
|
return formatted_zones
|
555
560
|
|