valetudo-map-parser 0.1.8__py3-none-any.whl → 0.1.9a0__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 +28 -13
- valetudo_map_parser/config/async_utils.py +93 -0
- valetudo_map_parser/config/auto_crop.py +312 -123
- valetudo_map_parser/config/color_utils.py +105 -0
- valetudo_map_parser/config/colors.py +662 -13
- valetudo_map_parser/config/drawable.py +613 -268
- valetudo_map_parser/config/drawable_elements.py +292 -0
- valetudo_map_parser/config/enhanced_drawable.py +324 -0
- valetudo_map_parser/config/optimized_element_map.py +406 -0
- valetudo_map_parser/config/rand256_parser.py +395 -0
- valetudo_map_parser/config/shared.py +94 -11
- valetudo_map_parser/config/types.py +105 -52
- valetudo_map_parser/config/utils.py +1025 -0
- valetudo_map_parser/hypfer_draw.py +464 -148
- valetudo_map_parser/hypfer_handler.py +366 -259
- valetudo_map_parser/hypfer_rooms_handler.py +599 -0
- valetudo_map_parser/map_data.py +56 -66
- valetudo_map_parser/rand256_handler.py +674 -0
- valetudo_map_parser/reimg_draw.py +68 -84
- valetudo_map_parser/rooms_handler.py +474 -0
- valetudo_map_parser-0.1.9a0.dist-info/METADATA +93 -0
- valetudo_map_parser-0.1.9a0.dist-info/RECORD +27 -0
- {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a0.dist-info}/WHEEL +1 -1
- valetudo_map_parser/config/rand25_parser.py +0 -398
- valetudo_map_parser/images_utils.py +0 -398
- valetudo_map_parser/rand25_handler.py +0 -455
- valetudo_map_parser-0.1.8.dist-info/METADATA +0 -23
- valetudo_map_parser-0.1.8.dist-info/RECORD +0 -20
- {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a0.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a0.dist-info}/NOTICE.txt +0 -0
|
@@ -2,170 +2,126 @@
|
|
|
2
2
|
Hypfer Image Handler Class.
|
|
3
3
|
It returns the PIL PNG image frame relative to the Map Data extrapolated from the vacuum json.
|
|
4
4
|
It also returns calibration, rooms data to the card and other images information to the camera.
|
|
5
|
-
Version:
|
|
5
|
+
Version: 0.1.9
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import asyncio
|
|
11
|
+
import numpy as np
|
|
12
12
|
|
|
13
13
|
from PIL import Image
|
|
14
14
|
|
|
15
|
+
from .config.async_utils import AsyncNumPy, AsyncPIL
|
|
16
|
+
from .config.auto_crop import AutoCrop
|
|
17
|
+
from .config.drawable_elements import DrawableElement
|
|
18
|
+
from .config.shared import CameraShared
|
|
19
|
+
from .config.utils import pil_to_webp_bytes
|
|
15
20
|
from .config.types import (
|
|
21
|
+
COLORS,
|
|
22
|
+
LOGGER,
|
|
16
23
|
CalibrationPoints,
|
|
17
|
-
|
|
18
|
-
ImageSize,
|
|
19
|
-
RobotPosition,
|
|
24
|
+
Colors,
|
|
20
25
|
RoomsProperties,
|
|
26
|
+
RoomStore,
|
|
27
|
+
WebPBytes,
|
|
28
|
+
JsonType,
|
|
21
29
|
)
|
|
22
|
-
from .config.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
resize_to_aspect_ratio,
|
|
29
|
-
)
|
|
30
|
-
from .hypfer_draw import (
|
|
31
|
-
ImageDraw as ImDraw,
|
|
30
|
+
from .config.utils import (
|
|
31
|
+
BaseHandler,
|
|
32
|
+
initialize_drawing_config,
|
|
33
|
+
manage_drawable_elements,
|
|
34
|
+
numpy_to_webp_bytes,
|
|
35
|
+
prepare_resize_params,
|
|
32
36
|
)
|
|
33
|
-
from .
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
from .hypfer_draw import ImageDraw as ImDraw
|
|
38
|
+
from .map_data import ImageData
|
|
39
|
+
from .rooms_handler import RoomsHandler
|
|
36
40
|
|
|
37
41
|
|
|
38
|
-
class HypferMapImageHandler:
|
|
42
|
+
class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
39
43
|
"""Map Image Handler Class.
|
|
40
44
|
This class is used to handle the image data and the drawing of the map."""
|
|
41
45
|
|
|
42
46
|
def __init__(self, shared_data: CameraShared):
|
|
43
47
|
"""Initialize the Map Image Handler."""
|
|
48
|
+
BaseHandler.__init__(self)
|
|
44
49
|
self.shared = shared_data # camera shared data
|
|
45
|
-
self
|
|
46
|
-
self.auto_crop = None # auto crop data to be calculate once.
|
|
50
|
+
AutoCrop.__init__(self, self)
|
|
47
51
|
self.calibration_data = None # camera shared data.
|
|
48
|
-
self.charger_pos = None # vacuum data charger position.
|
|
49
|
-
self.crop_area = None # module shared for calibration data.
|
|
50
|
-
self.crop_img_size = None # size of the image cropped calibration data.
|
|
51
52
|
self.data = ImageData # imported Image Data Module.
|
|
52
|
-
|
|
53
|
+
|
|
54
|
+
# Initialize drawing configuration using the shared utility function
|
|
55
|
+
self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(
|
|
56
|
+
self
|
|
57
|
+
)
|
|
58
|
+
|
|
53
59
|
self.go_to = None # vacuum go to data
|
|
54
60
|
self.img_hash = None # hash of the image calculated to check differences.
|
|
55
61
|
self.img_base_layer = None # numpy array store the map base layer.
|
|
56
|
-
self.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
self.path_pixels = None # vacuum path datas.
|
|
60
|
-
self.robot_in_room = None # vacuum room position.
|
|
61
|
-
self.robot_pos = None # vacuum coordinates.
|
|
62
|
-
self.room_propriety = None # vacuum segments data.
|
|
63
|
-
self.rooms_pos = None # vacuum room coordinates / name list.
|
|
62
|
+
self.img_work_layer = (
|
|
63
|
+
None # persistent working buffer to avoid per-frame allocations
|
|
64
|
+
)
|
|
64
65
|
self.active_zones = None # vacuum active zones.
|
|
65
|
-
self.frame_number = 0 # frame number of the image.
|
|
66
|
-
self.max_frames = 1024
|
|
67
|
-
self.zooming = False # zooming the image.
|
|
68
66
|
self.svg_wait = False # SVG image creation wait.
|
|
69
|
-
self.
|
|
70
|
-
self.trim_left = 0 # memory stored trims calculated once.
|
|
71
|
-
self.trim_right = 0 # memory stored trims calculated once.
|
|
72
|
-
self.trim_up = 0 # memory stored trims calculated once.
|
|
73
|
-
self.offset_top = self.shared.offset_top # offset top
|
|
74
|
-
self.offset_bottom = self.shared.offset_down # offset bottom
|
|
75
|
-
self.offset_left = self.shared.offset_left # offset left
|
|
76
|
-
self.offset_right = self.shared.offset_right # offset right
|
|
77
|
-
self.offset_x = 0 # offset x for the aspect ratio.
|
|
78
|
-
self.offset_y = 0 # offset y for the aspect ratio.
|
|
79
|
-
self.imd = ImDraw(self)
|
|
80
|
-
self.imu = ImUtils(self)
|
|
81
|
-
self.ac = AutoCrop(self)
|
|
82
|
-
self.colors_manager = ColorsManagment({})
|
|
83
|
-
self.rooms_colors = self.colors_manager.get_rooms_colors()
|
|
67
|
+
self.imd = ImDraw(self) # Image Draw class.
|
|
84
68
|
self.color_grey = (128, 128, 128, 255)
|
|
69
|
+
self.file_name = self.shared.file_name # file name of the vacuum.
|
|
70
|
+
self.rooms_handler = RoomsHandler(
|
|
71
|
+
self.file_name, self.drawing_config
|
|
72
|
+
) # Room data handler
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def get_corners(x_max, x_min, y_max, y_min):
|
|
76
|
+
"""Get the corners of the room."""
|
|
77
|
+
return [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
|
|
85
78
|
|
|
86
79
|
async def async_extract_room_properties(self, json_data) -> RoomsProperties:
|
|
87
80
|
"""Extract room properties from the JSON data."""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
pixel_size = json_data.get("pixelSize", [])
|
|
92
|
-
|
|
93
|
-
for layer in json_data.get("layers", []):
|
|
94
|
-
if layer["__class"] == "MapLayer":
|
|
95
|
-
meta_data = layer.get("metaData", {})
|
|
96
|
-
segment_id = meta_data.get("segmentId")
|
|
97
|
-
if segment_id is not None:
|
|
98
|
-
name = meta_data.get("name")
|
|
99
|
-
compressed_pixels = layer.get("compressedPixels", [])
|
|
100
|
-
pixels = self.data.sublist(compressed_pixels, 3)
|
|
101
|
-
# Calculate x and y min/max from compressed pixels
|
|
102
|
-
(
|
|
103
|
-
x_min,
|
|
104
|
-
y_min,
|
|
105
|
-
x_max,
|
|
106
|
-
y_max,
|
|
107
|
-
) = await self.data.async_get_rooms_coordinates(pixels, pixel_size)
|
|
108
|
-
corners = [
|
|
109
|
-
(x_min, y_min),
|
|
110
|
-
(x_max, y_min),
|
|
111
|
-
(x_max, y_max),
|
|
112
|
-
(x_min, y_max),
|
|
113
|
-
]
|
|
114
|
-
room_id = str(segment_id)
|
|
115
|
-
self.rooms_pos.append(
|
|
116
|
-
{
|
|
117
|
-
"name": name,
|
|
118
|
-
"corners": corners,
|
|
119
|
-
}
|
|
120
|
-
)
|
|
121
|
-
room_properties[room_id] = {
|
|
122
|
-
"number": segment_id,
|
|
123
|
-
"outline": corners,
|
|
124
|
-
"name": name,
|
|
125
|
-
"x": ((x_min + x_max) // 2),
|
|
126
|
-
"y": ((y_min + y_max) // 2),
|
|
127
|
-
}
|
|
81
|
+
room_properties = await self.rooms_handler.async_extract_room_properties(
|
|
82
|
+
json_data
|
|
83
|
+
)
|
|
128
84
|
if room_properties:
|
|
129
|
-
|
|
85
|
+
rooms = RoomStore(self.file_name, room_properties)
|
|
86
|
+
LOGGER.debug(
|
|
87
|
+
"%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
|
|
88
|
+
)
|
|
89
|
+
# Convert room_properties to the format expected by async_get_robot_in_room
|
|
90
|
+
self.rooms_pos = []
|
|
91
|
+
for room_id, room_data in room_properties.items():
|
|
92
|
+
self.rooms_pos.append(
|
|
93
|
+
{
|
|
94
|
+
"id": room_id,
|
|
95
|
+
"name": room_data["name"],
|
|
96
|
+
"outline": room_data["outline"],
|
|
97
|
+
}
|
|
98
|
+
)
|
|
130
99
|
else:
|
|
131
|
-
|
|
100
|
+
LOGGER.debug("%s: Rooms data not available!", self.file_name)
|
|
132
101
|
self.rooms_pos = None
|
|
133
102
|
return room_properties
|
|
134
103
|
|
|
135
|
-
async def _async_initialize_colors(self):
|
|
136
|
-
"""Initialize and return all required colors."""
|
|
137
|
-
return {
|
|
138
|
-
"color_wall": self.colors_manager.get_colour(SupportedColor.WALLS),
|
|
139
|
-
"color_no_go": self.colors_manager.get_colour(SupportedColor.NO_GO),
|
|
140
|
-
"color_go_to": self.colors_manager.get_colour(SupportedColor.GO_TO),
|
|
141
|
-
"color_robot": self.colors_manager.get_colour(SupportedColor.ROBOT),
|
|
142
|
-
"color_charger": self.colors_manager.get_colour(SupportedColor.CHARGER),
|
|
143
|
-
"color_move": self.colors_manager.get_colour(SupportedColor.PATH),
|
|
144
|
-
"color_background": self.colors_manager.get_colour(
|
|
145
|
-
SupportedColor.MAP_BACKGROUND
|
|
146
|
-
),
|
|
147
|
-
"color_zone_clean": self.colors_manager.get_colour(
|
|
148
|
-
SupportedColor.ZONE_CLEAN
|
|
149
|
-
),
|
|
150
|
-
}
|
|
151
|
-
|
|
152
104
|
# noinspection PyUnresolvedReferences,PyUnboundLocalVariable
|
|
153
105
|
async def async_get_image_from_json(
|
|
154
106
|
self,
|
|
155
|
-
m_json:
|
|
156
|
-
|
|
107
|
+
m_json: JsonType | None,
|
|
108
|
+
return_webp: bool = False,
|
|
109
|
+
) -> WebPBytes | Image.Image | None:
|
|
157
110
|
"""Get the image from the JSON data.
|
|
158
111
|
It uses the ImageDraw class to draw some of the elements of the image.
|
|
159
112
|
The robot itself will be drawn in this function as per some of the values are needed for other tasks.
|
|
160
113
|
@param m_json: The JSON data to use to draw the image.
|
|
161
|
-
@
|
|
114
|
+
@param return_webp: If True, return WebP bytes; if False, return PIL Image (default).
|
|
115
|
+
@return WebPBytes | Image.Image: WebP bytes or PIL Image depending on return_webp parameter.
|
|
162
116
|
"""
|
|
163
117
|
# Initialize the colors.
|
|
164
|
-
colors =
|
|
118
|
+
colors: Colors = {
|
|
119
|
+
name: self.shared.user_colors[idx] for idx, name in enumerate(COLORS)
|
|
120
|
+
}
|
|
165
121
|
# Check if the JSON data is not None else process the image.
|
|
166
122
|
try:
|
|
167
123
|
if m_json is not None:
|
|
168
|
-
|
|
124
|
+
LOGGER.debug("%s: Creating Image.", self.file_name)
|
|
169
125
|
# buffer json data
|
|
170
126
|
self.json_data = m_json
|
|
171
127
|
# Get the image size from the JSON data
|
|
@@ -190,135 +146,285 @@ class HypferMapImageHandler:
|
|
|
190
146
|
# Get the pixels size and layers from the JSON data
|
|
191
147
|
pixel_size = int(m_json["pixelSize"])
|
|
192
148
|
layers, active = self.data.find_layers(m_json["layers"], {}, [])
|
|
193
|
-
|
|
149
|
+
# Populate active_zones from the JSON data
|
|
150
|
+
self.active_zones = active
|
|
151
|
+
new_frame_hash = await self.calculate_array_hash(layers, active)
|
|
194
152
|
if self.frame_number == 0:
|
|
195
153
|
self.img_hash = new_frame_hash
|
|
196
|
-
# empty image
|
|
154
|
+
# Create empty image
|
|
197
155
|
img_np_array = await self.draw.create_empty_image(
|
|
198
|
-
size_x, size_y, colors["
|
|
156
|
+
size_x, size_y, colors["background"]
|
|
199
157
|
)
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
158
|
+
# Draw layers and segments if enabled
|
|
159
|
+
room_id = 0
|
|
160
|
+
# Keep track of disabled rooms to skip their walls later
|
|
161
|
+
disabled_rooms = set()
|
|
162
|
+
|
|
163
|
+
if self.drawing_config.is_enabled(DrawableElement.FLOOR):
|
|
164
|
+
# First pass: identify disabled rooms
|
|
165
|
+
for layer_type, compressed_pixels_list in layers.items():
|
|
166
|
+
# Check if this is a room layer
|
|
167
|
+
if layer_type == "segment":
|
|
168
|
+
# The room_id is the current room being processed (0-based index)
|
|
169
|
+
# We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
|
|
170
|
+
current_room_id = room_id + 1
|
|
171
|
+
if 1 <= current_room_id <= 15:
|
|
172
|
+
room_element = getattr(
|
|
173
|
+
DrawableElement, f"ROOM_{current_room_id}", None
|
|
174
|
+
)
|
|
175
|
+
if (
|
|
176
|
+
room_element
|
|
177
|
+
and not self.drawing_config.is_enabled(
|
|
178
|
+
room_element
|
|
179
|
+
)
|
|
180
|
+
):
|
|
181
|
+
# Add this room to the disabled rooms set
|
|
182
|
+
disabled_rooms.add(room_id)
|
|
183
|
+
LOGGER.debug(
|
|
184
|
+
"%s: Room %d is disabled and will be skipped",
|
|
185
|
+
self.file_name,
|
|
186
|
+
current_room_id,
|
|
187
|
+
)
|
|
188
|
+
room_id = (
|
|
189
|
+
room_id + 1
|
|
190
|
+
) % 16 # Cycle room_id back to 0 after 15
|
|
191
|
+
|
|
192
|
+
# Reset room_id for the actual drawing pass
|
|
193
|
+
room_id = 0
|
|
194
|
+
|
|
195
|
+
# Second pass: draw enabled rooms and walls
|
|
196
|
+
for layer_type, compressed_pixels_list in layers.items():
|
|
197
|
+
# Check if this is a room layer
|
|
198
|
+
is_room_layer = layer_type == "segment"
|
|
199
|
+
|
|
200
|
+
# If it's a room layer, check if the specific room is enabled
|
|
201
|
+
if is_room_layer:
|
|
202
|
+
# The room_id is the current room being processed (0-based index)
|
|
203
|
+
# We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
|
|
204
|
+
current_room_id = room_id + 1
|
|
205
|
+
if 1 <= current_room_id <= 15:
|
|
206
|
+
room_element = getattr(
|
|
207
|
+
DrawableElement, f"ROOM_{current_room_id}", None
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Skip this room if it's disabled
|
|
211
|
+
if not self.drawing_config.is_enabled(room_element):
|
|
212
|
+
room_id = (
|
|
213
|
+
room_id + 1
|
|
214
|
+
) % 16 # Increment room_id even if we skip
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# Draw the layer ONLY if enabled
|
|
218
|
+
is_wall_layer = layer_type == "wall"
|
|
219
|
+
if is_wall_layer:
|
|
220
|
+
# Skip walls entirely if disabled
|
|
221
|
+
if not self.drawing_config.is_enabled(
|
|
222
|
+
DrawableElement.WALL
|
|
223
|
+
):
|
|
224
|
+
continue
|
|
225
|
+
# Draw the layer
|
|
226
|
+
(
|
|
227
|
+
room_id,
|
|
228
|
+
img_np_array,
|
|
229
|
+
) = await self.imd.async_draw_base_layer(
|
|
230
|
+
img_np_array,
|
|
231
|
+
compressed_pixels_list,
|
|
232
|
+
layer_type,
|
|
233
|
+
colors["wall"],
|
|
234
|
+
colors["zone_clean"],
|
|
235
|
+
pixel_size,
|
|
236
|
+
disabled_rooms if layer_type == "wall" else None,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Draw the virtual walls if enabled
|
|
240
|
+
if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
|
|
241
|
+
img_np_array = await self.imd.async_draw_virtual_walls(
|
|
242
|
+
m_json, img_np_array, colors["no_go"]
|
|
209
243
|
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
244
|
+
|
|
245
|
+
# Draw charger if enabled
|
|
246
|
+
if self.drawing_config.is_enabled(DrawableElement.CHARGER):
|
|
247
|
+
img_np_array = await self.imd.async_draw_charger(
|
|
248
|
+
img_np_array, entity_dict, colors["charger"]
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Draw obstacles if enabled
|
|
252
|
+
if self.drawing_config.is_enabled(DrawableElement.OBSTACLE):
|
|
253
|
+
self.shared.obstacles_pos = self.data.get_obstacles(entity_dict)
|
|
254
|
+
if self.shared.obstacles_pos:
|
|
255
|
+
img_np_array = await self.imd.async_draw_obstacle(
|
|
256
|
+
img_np_array, self.shared.obstacles_pos, colors["no_go"]
|
|
257
|
+
)
|
|
222
258
|
# Robot and rooms position
|
|
223
259
|
if (room_id > 0) and not self.room_propriety:
|
|
224
260
|
self.room_propriety = await self.async_extract_room_properties(
|
|
225
261
|
self.json_data
|
|
226
262
|
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
263
|
+
|
|
264
|
+
# Ensure room data is available for robot room detection (even if not extracted above)
|
|
265
|
+
if not self.rooms_pos and not self.room_propriety:
|
|
266
|
+
self.room_propriety = await self.async_extract_room_properties(
|
|
267
|
+
self.json_data
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Always check robot position for zooming (moved outside the condition)
|
|
271
|
+
if self.rooms_pos and robot_position and robot_position_angle:
|
|
272
|
+
self.robot_pos = await self.imd.async_get_robot_in_room(
|
|
273
|
+
robot_x=(robot_position[0]),
|
|
274
|
+
robot_y=(robot_position[1]),
|
|
275
|
+
angle=robot_position_angle,
|
|
276
|
+
)
|
|
277
|
+
LOGGER.info("%s: Completed base Layers", self.file_name)
|
|
234
278
|
# Copy the new array in base layer.
|
|
235
|
-
self.img_base_layer = await self.
|
|
279
|
+
self.img_base_layer = await self.async_copy_array(img_np_array)
|
|
280
|
+
|
|
236
281
|
self.shared.frame_number = self.frame_number
|
|
237
282
|
self.frame_number += 1
|
|
238
283
|
if (self.frame_number >= self.max_frames) or (
|
|
239
284
|
new_frame_hash != self.img_hash
|
|
240
285
|
):
|
|
241
286
|
self.frame_number = 0
|
|
242
|
-
|
|
287
|
+
LOGGER.debug(
|
|
243
288
|
"%s: %s at Frame Number: %s",
|
|
244
289
|
self.file_name,
|
|
245
290
|
str(self.json_id),
|
|
246
291
|
str(self.frame_number),
|
|
247
292
|
)
|
|
248
|
-
#
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
293
|
+
# Ensure persistent working buffer exists and matches base (allocate only when needed)
|
|
294
|
+
if (
|
|
295
|
+
self.img_work_layer is None
|
|
296
|
+
or self.img_work_layer.shape != self.img_base_layer.shape
|
|
297
|
+
or self.img_work_layer.dtype != self.img_base_layer.dtype
|
|
298
|
+
):
|
|
299
|
+
self.img_work_layer = np.empty_like(self.img_base_layer)
|
|
300
|
+
|
|
301
|
+
# Copy the base layer into the persistent working buffer (no new allocation per frame)
|
|
302
|
+
np.copyto(self.img_work_layer, self.img_base_layer)
|
|
303
|
+
img_np_array = self.img_work_layer
|
|
304
|
+
|
|
305
|
+
# Prepare parallel data extraction tasks
|
|
306
|
+
data_tasks = []
|
|
307
|
+
|
|
308
|
+
# Prepare zone data extraction
|
|
309
|
+
if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
|
|
310
|
+
data_tasks.append(self._prepare_zone_data(m_json))
|
|
311
|
+
|
|
312
|
+
# Prepare go_to flag data extraction
|
|
313
|
+
if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
|
|
314
|
+
data_tasks.append(self._prepare_goto_data(entity_dict))
|
|
315
|
+
|
|
316
|
+
# Prepare path data extraction
|
|
317
|
+
path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
|
|
318
|
+
LOGGER.info(
|
|
319
|
+
"%s: PATH element enabled: %s", self.file_name, path_enabled
|
|
265
320
|
)
|
|
321
|
+
if path_enabled:
|
|
322
|
+
LOGGER.info("%s: Drawing path", self.file_name)
|
|
323
|
+
data_tasks.append(self._prepare_path_data(m_json))
|
|
324
|
+
|
|
325
|
+
# Await all data preparation tasks if any were created
|
|
326
|
+
if data_tasks:
|
|
327
|
+
await asyncio.gather(*data_tasks)
|
|
328
|
+
|
|
329
|
+
# Process drawing operations sequentially (since they modify the same array)
|
|
330
|
+
# Draw zones if enabled
|
|
331
|
+
if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
|
|
332
|
+
img_np_array = await self.imd.async_draw_zones(
|
|
333
|
+
m_json, img_np_array, colors["zone_clean"], colors["no_go"]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Draw the go_to target flag if enabled
|
|
337
|
+
if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
|
|
338
|
+
img_np_array = await self.imd.draw_go_to_flag(
|
|
339
|
+
img_np_array, entity_dict, colors["go_to"]
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Draw paths if enabled
|
|
343
|
+
if path_enabled:
|
|
344
|
+
img_np_array = await self.imd.async_draw_paths(
|
|
345
|
+
img_np_array, m_json, colors["move"], self.color_grey
|
|
346
|
+
)
|
|
347
|
+
else:
|
|
348
|
+
LOGGER.info("%s: Skipping path drawing", self.file_name)
|
|
349
|
+
|
|
266
350
|
# Check if the robot is docked.
|
|
267
351
|
if self.shared.vacuum_state == "docked":
|
|
268
352
|
# Adjust the robot angle.
|
|
269
353
|
robot_position_angle -= 180
|
|
270
354
|
|
|
271
|
-
if
|
|
355
|
+
# Draw the robot if enabled
|
|
356
|
+
if robot_pos and self.drawing_config.is_enabled(DrawableElement.ROBOT):
|
|
357
|
+
# Get robot color (allows for customization)
|
|
358
|
+
robot_color = self.drawing_config.get_property(
|
|
359
|
+
DrawableElement.ROBOT, "color", colors["robot"]
|
|
360
|
+
)
|
|
361
|
+
|
|
272
362
|
# Draw the robot
|
|
273
363
|
img_np_array = await self.draw.robot(
|
|
274
364
|
layers=img_np_array,
|
|
275
365
|
x=robot_position[0],
|
|
276
366
|
y=robot_position[1],
|
|
277
367
|
angle=robot_position_angle,
|
|
278
|
-
fill=
|
|
368
|
+
fill=robot_color,
|
|
279
369
|
robot_state=self.shared.vacuum_state,
|
|
280
370
|
)
|
|
371
|
+
|
|
372
|
+
# Update element map for robot position
|
|
373
|
+
if (
|
|
374
|
+
hasattr(self.shared, "element_map")
|
|
375
|
+
and self.shared.element_map is not None
|
|
376
|
+
):
|
|
377
|
+
update_element_map_with_robot(
|
|
378
|
+
self.shared.element_map,
|
|
379
|
+
robot_position,
|
|
380
|
+
DrawableElement.ROBOT,
|
|
381
|
+
)
|
|
382
|
+
# Synchronize zooming state from ImageDraw to handler before auto-crop
|
|
383
|
+
self.zooming = self.imd.img_h.zooming
|
|
384
|
+
|
|
281
385
|
# Resize the image
|
|
282
|
-
img_np_array = await self.
|
|
386
|
+
img_np_array = await self.async_auto_trim_and_zoom_image(
|
|
283
387
|
img_np_array,
|
|
284
|
-
colors["
|
|
388
|
+
colors["background"],
|
|
285
389
|
int(self.shared.margins),
|
|
286
390
|
int(self.shared.image_rotate),
|
|
287
391
|
self.zooming,
|
|
288
392
|
)
|
|
289
393
|
# If the image is None return None and log the error.
|
|
290
394
|
if img_np_array is None:
|
|
291
|
-
|
|
395
|
+
LOGGER.warning("%s: Image array is None.", self.file_name)
|
|
292
396
|
return None
|
|
293
397
|
|
|
294
|
-
#
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
self
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
398
|
+
# Handle resizing if needed, then return based on format preference
|
|
399
|
+
if self.check_zoom_and_aspect_ratio():
|
|
400
|
+
# Convert to PIL for resizing
|
|
401
|
+
pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
|
|
402
|
+
del img_np_array
|
|
403
|
+
resize_params = prepare_resize_params(self, pil_img, False)
|
|
404
|
+
resized_image = await self.async_resize_images(resize_params)
|
|
405
|
+
|
|
406
|
+
# Return WebP bytes or PIL Image based on parameter
|
|
407
|
+
if return_webp:
|
|
408
|
+
webp_bytes = await pil_to_webp_bytes(resized_image)
|
|
409
|
+
return webp_bytes
|
|
410
|
+
else:
|
|
411
|
+
return resized_image
|
|
412
|
+
else:
|
|
413
|
+
# Return WebP bytes or PIL Image based on parameter
|
|
414
|
+
if return_webp:
|
|
415
|
+
# Convert directly from NumPy to WebP for better performance
|
|
416
|
+
webp_bytes = await numpy_to_webp_bytes(img_np_array)
|
|
417
|
+
del img_np_array
|
|
418
|
+
LOGGER.debug("%s: Frame Completed.", self.file_name)
|
|
419
|
+
return webp_bytes
|
|
420
|
+
else:
|
|
421
|
+
# Convert to PIL Image (original behavior)
|
|
422
|
+
pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
|
|
423
|
+
del img_np_array
|
|
424
|
+
LOGGER.debug("%s: Frame Completed.", self.file_name)
|
|
425
|
+
return pil_img
|
|
320
426
|
except (RuntimeError, RuntimeWarning) as e:
|
|
321
|
-
|
|
427
|
+
LOGGER.warning(
|
|
322
428
|
"%s: Error %s during image creation.",
|
|
323
429
|
self.file_name,
|
|
324
430
|
str(e),
|
|
@@ -326,38 +432,18 @@ class HypferMapImageHandler:
|
|
|
326
432
|
)
|
|
327
433
|
return None
|
|
328
434
|
|
|
329
|
-
def get_frame_number(self) -> int:
|
|
330
|
-
"""Return the frame number of the image."""
|
|
331
|
-
return self.frame_number
|
|
332
|
-
|
|
333
|
-
def get_robot_position(self) -> RobotPosition | None:
|
|
334
|
-
"""Return the robot position."""
|
|
335
|
-
return self.robot_pos
|
|
336
|
-
|
|
337
|
-
def get_charger_position(self) -> ChargerPosition | None:
|
|
338
|
-
"""Return the charger position."""
|
|
339
|
-
return self.charger_pos
|
|
340
|
-
|
|
341
|
-
def get_img_size(self) -> ImageSize | None:
|
|
342
|
-
"""Return the size of the image."""
|
|
343
|
-
return self.img_size
|
|
344
|
-
|
|
345
|
-
def get_json_id(self) -> str | None:
|
|
346
|
-
"""Return the JSON ID from the image."""
|
|
347
|
-
return self.json_id
|
|
348
|
-
|
|
349
435
|
async def async_get_rooms_attributes(self) -> RoomsProperties:
|
|
350
436
|
"""Get the rooms attributes from the JSON data.
|
|
351
437
|
:return: The rooms attribute's."""
|
|
352
438
|
if self.room_propriety:
|
|
353
439
|
return self.room_propriety
|
|
354
440
|
if self.json_data:
|
|
355
|
-
|
|
441
|
+
LOGGER.debug("Checking %s Rooms data..", self.file_name)
|
|
356
442
|
self.room_propriety = await self.async_extract_room_properties(
|
|
357
443
|
self.json_data
|
|
358
444
|
)
|
|
359
445
|
if self.room_propriety:
|
|
360
|
-
|
|
446
|
+
LOGGER.debug("Got %s Rooms Attributes.", self.file_name)
|
|
361
447
|
return self.room_propriety
|
|
362
448
|
|
|
363
449
|
def get_calibration_data(self) -> CalibrationPoints:
|
|
@@ -365,20 +451,12 @@ class HypferMapImageHandler:
|
|
|
365
451
|
this will create the attribute calibration points."""
|
|
366
452
|
calibration_data = []
|
|
367
453
|
rotation_angle = self.shared.image_rotate
|
|
368
|
-
|
|
454
|
+
LOGGER.info("Getting %s Calibrations points.", self.file_name)
|
|
369
455
|
|
|
370
456
|
# Define the map points (fixed)
|
|
371
|
-
map_points =
|
|
372
|
-
{"x": 0, "y": 0}, # Top-left corner 0
|
|
373
|
-
{"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
|
|
374
|
-
{
|
|
375
|
-
"x": self.crop_img_size[0],
|
|
376
|
-
"y": self.crop_img_size[1],
|
|
377
|
-
}, # Bottom-right corner 2
|
|
378
|
-
{"x": 0, "y": self.crop_img_size[1]}, # Bottom-left corner (optional) 3
|
|
379
|
-
]
|
|
457
|
+
map_points = self.get_map_points()
|
|
380
458
|
# Calculate the calibration points in the vacuum coordinate system
|
|
381
|
-
vacuum_points = self.
|
|
459
|
+
vacuum_points = self.get_vacuum_points(rotation_angle)
|
|
382
460
|
|
|
383
461
|
# Create the calibration data for each point
|
|
384
462
|
for vacuum_point, map_point in zip(vacuum_points, map_points):
|
|
@@ -387,32 +465,61 @@ class HypferMapImageHandler:
|
|
|
387
465
|
del vacuum_points, map_points, calibration_point, rotation_angle # free memory.
|
|
388
466
|
return calibration_data
|
|
389
467
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
:raises ValueError: If any input parameters are negative
|
|
401
|
-
"""
|
|
468
|
+
# Element selection methods
|
|
469
|
+
def enable_element(self, element_code: DrawableElement) -> None:
|
|
470
|
+
"""Enable drawing of a specific element."""
|
|
471
|
+
self.drawing_config.enable_element(element_code)
|
|
472
|
+
LOGGER.info(
|
|
473
|
+
"%s: Enabled element %s, now enabled: %s",
|
|
474
|
+
self.file_name,
|
|
475
|
+
element_code.name,
|
|
476
|
+
self.drawing_config.is_enabled(element_code),
|
|
477
|
+
)
|
|
402
478
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
479
|
+
def disable_element(self, element_code: DrawableElement) -> None:
|
|
480
|
+
"""Disable drawing of a specific element."""
|
|
481
|
+
manage_drawable_elements(self, "disable", element_code=element_code)
|
|
482
|
+
|
|
483
|
+
def set_elements(self, element_codes: list[DrawableElement]) -> None:
|
|
484
|
+
"""Enable only the specified elements, disable all others."""
|
|
485
|
+
manage_drawable_elements(self, "set_elements", element_codes=element_codes)
|
|
486
|
+
|
|
487
|
+
def set_element_property(
|
|
488
|
+
self, element_code: DrawableElement, property_name: str, value
|
|
489
|
+
) -> None:
|
|
490
|
+
"""Set a drawing property for an element."""
|
|
491
|
+
manage_drawable_elements(
|
|
492
|
+
self,
|
|
493
|
+
"set_property",
|
|
494
|
+
element_code=element_code,
|
|
495
|
+
property_name=property_name,
|
|
496
|
+
value=value,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
@staticmethod
|
|
500
|
+
async def async_copy_array(original_array):
|
|
501
|
+
"""Copy the array."""
|
|
502
|
+
return await AsyncNumPy.async_copy(original_array)
|
|
503
|
+
|
|
504
|
+
async def _prepare_zone_data(self, m_json):
|
|
505
|
+
"""Prepare zone data for parallel processing."""
|
|
506
|
+
await asyncio.sleep(0) # Yield control
|
|
507
|
+
try:
|
|
508
|
+
return self.data.find_zone_entities(m_json)
|
|
509
|
+
except (ValueError, KeyError):
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
@staticmethod
|
|
513
|
+
async def _prepare_goto_data(entity_dict):
|
|
514
|
+
"""Prepare go-to flag data for parallel processing."""
|
|
515
|
+
await asyncio.sleep(0) # Yield control
|
|
516
|
+
# Extract go-to target data from entity_dict
|
|
517
|
+
return entity_dict.get("go_to_target", None)
|
|
518
|
+
|
|
519
|
+
async def _prepare_path_data(self, m_json):
|
|
520
|
+
"""Prepare path data for parallel processing."""
|
|
521
|
+
await asyncio.sleep(0) # Yield control
|
|
522
|
+
try:
|
|
523
|
+
return self.data.find_paths_entities(m_json)
|
|
524
|
+
except (ValueError, KeyError):
|
|
525
|
+
return None
|