valetudo-map-parser 0.1.8__py3-none-any.whl → 0.1.9a2__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 +19 -12
- valetudo_map_parser/config/auto_crop.py +174 -116
- valetudo_map_parser/config/color_utils.py +105 -0
- valetudo_map_parser/config/colors.py +662 -13
- valetudo_map_parser/config/drawable.py +624 -279
- 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/rand25_parser.py +42 -28
- valetudo_map_parser/config/room_outline.py +148 -0
- valetudo_map_parser/config/shared.py +73 -6
- valetudo_map_parser/config/types.py +102 -51
- valetudo_map_parser/config/utils.py +841 -0
- valetudo_map_parser/hypfer_draw.py +398 -132
- valetudo_map_parser/hypfer_handler.py +259 -241
- valetudo_map_parser/hypfer_rooms_handler.py +599 -0
- valetudo_map_parser/map_data.py +45 -64
- valetudo_map_parser/rand25_handler.py +429 -310
- valetudo_map_parser/reimg_draw.py +55 -74
- valetudo_map_parser/rooms_handler.py +470 -0
- valetudo_map_parser-0.1.9a2.dist-info/METADATA +93 -0
- valetudo_map_parser-0.1.9a2.dist-info/RECORD +27 -0
- {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a2.dist-info}/WHEEL +1 -1
- valetudo_map_parser/images_utils.py +0 -398
- 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.9a2.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a2.dist-info}/NOTICE.txt +0 -0
@@ -2,147 +2,131 @@
|
|
2
2
|
Image Handler Module for Valetudo Re Vacuums.
|
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.b42
|
6
6
|
"""
|
7
7
|
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
10
|
import logging
|
11
11
|
import uuid
|
12
|
+
from typing import Any
|
12
13
|
|
13
|
-
|
14
|
+
import numpy as np
|
15
|
+
from PIL import Image
|
14
16
|
|
15
|
-
from .config.types import COLORS, DEFAULT_IMAGE_SIZE, DEFAULT_PIXEL_SIZE
|
16
|
-
from .config.types import Colors, JsonType, PilPNG, RobotPosition, RoomsProperties
|
17
17
|
from .config.auto_crop import AutoCrop
|
18
|
-
from .
|
19
|
-
from .
|
18
|
+
from .config.drawable_elements import DrawableElement
|
19
|
+
from .config.types import (
|
20
|
+
COLORS,
|
21
|
+
DEFAULT_IMAGE_SIZE,
|
22
|
+
DEFAULT_PIXEL_SIZE,
|
23
|
+
Colors,
|
24
|
+
JsonType,
|
25
|
+
PilPNG,
|
26
|
+
RobotPosition,
|
27
|
+
RoomsProperties,
|
28
|
+
RoomStore,
|
29
|
+
)
|
30
|
+
from .config.utils import (
|
31
|
+
BaseHandler,
|
32
|
+
initialize_drawing_config,
|
33
|
+
manage_drawable_elements,
|
34
|
+
prepare_resize_params,
|
35
|
+
)
|
36
|
+
from .map_data import RandImageData
|
20
37
|
from .reimg_draw import ImageDraw
|
38
|
+
from .rooms_handler import RandRoomsHandler
|
39
|
+
|
21
40
|
|
22
41
|
_LOGGER = logging.getLogger(__name__)
|
23
42
|
|
24
43
|
|
25
44
|
# noinspection PyTypeChecker
|
26
|
-
class ReImageHandler(
|
45
|
+
class ReImageHandler(BaseHandler, AutoCrop):
|
27
46
|
"""
|
28
47
|
Image Handler for Valetudo Re Vacuums.
|
29
48
|
"""
|
30
49
|
|
31
|
-
def __init__(self,
|
50
|
+
def __init__(self, shared_data):
|
51
|
+
BaseHandler.__init__(self)
|
52
|
+
self.shared = shared_data # Shared data
|
53
|
+
AutoCrop.__init__(self, self)
|
32
54
|
self.auto_crop = None # Auto crop flag
|
33
55
|
self.segment_data = None # Segment data
|
34
56
|
self.outlines = None # Outlines data
|
35
57
|
self.calibration_data = None # Calibration data
|
36
|
-
self.
|
37
|
-
|
38
|
-
|
39
|
-
self.
|
40
|
-
|
41
|
-
|
58
|
+
self.data = RandImageData # Image Data
|
59
|
+
|
60
|
+
# Initialize drawing configuration using the shared utility function
|
61
|
+
self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(
|
62
|
+
self
|
63
|
+
)
|
42
64
|
self.go_to = None # Go to position data
|
43
65
|
self.img_base_layer = None # Base image layer
|
44
|
-
self.img_rotate =
|
45
|
-
self.img_size = None # Image size
|
46
|
-
self.json_data = None # Json data
|
47
|
-
self.json_id = None # Json id
|
48
|
-
self.path_pixels = None # Path pixels data
|
49
|
-
self.robot_in_room = None # Robot in room data
|
50
|
-
self.robot_pos = None # Robot position
|
66
|
+
self.img_rotate = shared_data.image_rotate # Image rotation
|
51
67
|
self.room_propriety = None # Room propriety data
|
52
|
-
self.rooms_pos = None # Rooms position data
|
53
|
-
self.shared = camera_shared # Shared data
|
54
68
|
self.active_zones = None # Active zones
|
55
|
-
self.trim_down = None # Trim down
|
56
|
-
self.trim_left = None # Trim left
|
57
|
-
self.trim_right = None # Trim right
|
58
|
-
self.trim_up = None # Trim up
|
59
|
-
self.zooming = False # Zooming flag
|
60
69
|
self.file_name = self.shared.file_name # File name
|
61
|
-
self.offset_x = 0 # offset x for the aspect ratio.
|
62
|
-
self.offset_y = 0 # offset y for the aspect ratio.
|
63
|
-
self.offset_top = self.shared.offset_top # offset top
|
64
|
-
self.offset_bottom = self.shared.offset_down # offset bottom
|
65
|
-
self.offset_left = self.shared.offset_left # offset left
|
66
|
-
self.offset_right = self.shared.offset_right # offset right
|
67
70
|
self.imd = ImageDraw(self) # Image Draw
|
68
|
-
self.
|
69
|
-
self.ac = AutoCrop(self)
|
71
|
+
self.rooms_handler = RandRoomsHandler(self.file_name, self.drawing_config) # Room data handler
|
70
72
|
|
71
73
|
async def extract_room_properties(
|
72
74
|
self, json_data: JsonType, destinations: JsonType
|
73
75
|
) -> RoomsProperties:
|
74
76
|
"""Extract the room properties."""
|
75
|
-
unsorted_id =
|
76
|
-
size_x, size_y =
|
77
|
-
top, left =
|
77
|
+
# unsorted_id = RandImageData.get_rrm_segments_ids(json_data)
|
78
|
+
size_x, size_y = RandImageData.get_rrm_image_size(json_data)
|
79
|
+
top, left = RandImageData.get_rrm_image_position(json_data)
|
78
80
|
try:
|
79
81
|
if not self.segment_data or not self.outlines:
|
80
82
|
(
|
81
83
|
self.segment_data,
|
82
84
|
self.outlines,
|
83
|
-
) = await
|
85
|
+
) = await RandImageData.async_get_rrm_segments(
|
84
86
|
json_data, size_x, size_y, top, left, True
|
85
87
|
)
|
88
|
+
|
86
89
|
dest_json = destinations
|
87
|
-
room_data = dict(dest_json).get("rooms", [])
|
88
90
|
zones_data = dict(dest_json).get("zones", [])
|
89
91
|
points_data = dict(dest_json).get("spots", [])
|
90
|
-
|
92
|
+
|
93
|
+
# Use the RandRoomsHandler to extract room properties
|
94
|
+
room_properties = await self.rooms_handler.async_extract_room_properties(
|
95
|
+
json_data, dest_json
|
96
|
+
)
|
97
|
+
|
98
|
+
# Update self.rooms_pos from room_properties for compatibility with other methods
|
91
99
|
self.rooms_pos = []
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
"number": int(room_id),
|
120
|
-
"outline": corners,
|
121
|
-
"name": name,
|
122
|
-
"x": (x_min + x_max) // 2,
|
123
|
-
"y": (y_min + y_max) // 2,
|
124
|
-
}
|
125
|
-
# get the zones and points data
|
126
|
-
zone_properties = await self.imu.async_zone_propriety(zones_data)
|
127
|
-
# get the points data
|
128
|
-
point_properties = await self.imu.async_points_propriety(points_data)
|
129
|
-
|
130
|
-
if room_properties != {}:
|
131
|
-
if zone_properties != {}:
|
132
|
-
_LOGGER.debug("Rooms and Zones, data extracted!")
|
133
|
-
else:
|
134
|
-
_LOGGER.debug("Rooms, data extracted!")
|
135
|
-
elif zone_properties != {}:
|
136
|
-
_LOGGER.debug("Zones, data extracted!")
|
137
|
-
else:
|
138
|
-
self.rooms_pos = None
|
139
|
-
_LOGGER.debug(
|
140
|
-
f"{self.file_name}: Rooms and Zones data not available!"
|
141
|
-
)
|
142
|
-
return room_properties, zone_properties, point_properties
|
143
|
-
except Exception as e:
|
100
|
+
for room_id, room_data in room_properties.items():
|
101
|
+
self.rooms_pos.append(
|
102
|
+
{"name": room_data["name"], "outline": room_data["outline"]}
|
103
|
+
)
|
104
|
+
|
105
|
+
# get the zones and points data
|
106
|
+
zone_properties = await self.async_zone_propriety(zones_data)
|
107
|
+
# get the points data
|
108
|
+
point_properties = await self.async_points_propriety(points_data)
|
109
|
+
|
110
|
+
if room_properties or zone_properties:
|
111
|
+
extracted_data = [
|
112
|
+
f"{len(room_properties)} Rooms" if room_properties else None,
|
113
|
+
f"{len(zone_properties)} Zones" if zone_properties else None,
|
114
|
+
]
|
115
|
+
extracted_data = ", ".join(filter(None, extracted_data))
|
116
|
+
_LOGGER.debug("Extracted data: %s", extracted_data)
|
117
|
+
else:
|
118
|
+
self.rooms_pos = None
|
119
|
+
_LOGGER.debug(
|
120
|
+
"%s: Rooms and Zones data not available!", self.file_name
|
121
|
+
)
|
122
|
+
|
123
|
+
rooms = RoomStore(self.file_name, room_properties)
|
124
|
+
_LOGGER.debug("Rooms Data: %s", rooms.get_rooms())
|
125
|
+
return room_properties, zone_properties, point_properties
|
126
|
+
except (RuntimeError, ValueError) as e:
|
144
127
|
_LOGGER.debug(
|
145
|
-
|
128
|
+
"No rooms Data or Error in extract_room_properties: %s",
|
129
|
+
e,
|
146
130
|
exc_info=True,
|
147
131
|
)
|
148
132
|
return None, None, None
|
@@ -160,164 +144,175 @@ class ReImageHandler(object):
|
|
160
144
|
|
161
145
|
try:
|
162
146
|
if (m_json is not None) and (not isinstance(m_json, tuple)):
|
163
|
-
_LOGGER.info(
|
164
|
-
# buffer json data
|
147
|
+
_LOGGER.info("%s: Composing the image for the camera.", self.file_name)
|
165
148
|
self.json_data = m_json
|
166
|
-
# get the image size
|
167
149
|
size_x, size_y = self.data.get_rrm_image_size(m_json)
|
168
|
-
##########################
|
169
150
|
self.img_size = DEFAULT_IMAGE_SIZE
|
170
|
-
###########################
|
171
151
|
self.json_id = str(uuid.uuid4()) # image id
|
172
|
-
_LOGGER.info(
|
173
|
-
|
152
|
+
_LOGGER.info("Vacuum Data ID: %s", self.json_id)
|
153
|
+
|
174
154
|
(
|
175
|
-
|
155
|
+
img_np_array,
|
176
156
|
robot_position,
|
177
157
|
robot_position_angle,
|
178
|
-
) = await self.
|
179
|
-
|
180
|
-
room_id, img_np_array = await self.imd.async_draw_base_layer(
|
181
|
-
m_json,
|
182
|
-
size_x,
|
183
|
-
size_y,
|
184
|
-
colors["wall"],
|
185
|
-
colors["zone_clean"],
|
186
|
-
colors["background"],
|
187
|
-
DEFAULT_PIXEL_SIZE,
|
188
|
-
)
|
189
|
-
_LOGGER.info(f"{self.file_name}: Completed base Layers")
|
190
|
-
if (room_id > 0) and not self.room_propriety:
|
191
|
-
self.room_propriety = await self.get_rooms_attributes(
|
192
|
-
destinations
|
193
|
-
)
|
194
|
-
if self.rooms_pos:
|
195
|
-
self.robot_pos = await self.async_get_robot_in_room(
|
196
|
-
(robot_position[0] * 10),
|
197
|
-
(robot_position[1] * 10),
|
198
|
-
robot_position_angle,
|
199
|
-
)
|
200
|
-
self.img_base_layer = await self.imd.async_copy_array(img_np_array)
|
201
|
-
|
202
|
-
# If there is a zone clean we draw it now.
|
203
|
-
self.frame_number += 1
|
204
|
-
img_np_array = await self.imd.async_copy_array(self.img_base_layer)
|
205
|
-
_LOGGER.debug(f"{self.file_name}: Frame number {self.frame_number}")
|
206
|
-
if self.frame_number > 5:
|
207
|
-
self.frame_number = 0
|
208
|
-
# All below will be drawn each time
|
209
|
-
# charger
|
210
|
-
img_np_array, self.charger_pos = await self.imd.async_draw_charger(
|
211
|
-
img_np_array, m_json, colors["charger"]
|
212
|
-
)
|
213
|
-
# zone clean
|
214
|
-
img_np_array = await self.imd.async_draw_zones(
|
215
|
-
m_json, img_np_array, colors["zone_clean"]
|
216
|
-
)
|
217
|
-
# virtual walls
|
218
|
-
img_np_array = await self.imd.async_draw_virtual_restrictions(
|
219
|
-
m_json, img_np_array, colors["no_go"]
|
220
|
-
)
|
221
|
-
# draw path
|
222
|
-
img_np_array = await self.imd.async_draw_path(
|
223
|
-
img_np_array, m_json, colors["move"]
|
224
|
-
)
|
225
|
-
# go to flag and predicted path
|
226
|
-
await self.imd.async_draw_go_to_flag(
|
227
|
-
img_np_array, m_json, colors["go_to"]
|
228
|
-
)
|
229
|
-
# draw the robot
|
230
|
-
img_np_array = await self.imd.async_draw_robot_on_map(
|
231
|
-
img_np_array, robot_position, robot_position_angle, colors["robot"]
|
158
|
+
) = await self._setup_robot_and_image(
|
159
|
+
m_json, size_x, size_y, colors, destinations
|
232
160
|
)
|
161
|
+
|
162
|
+
# Increment frame number
|
163
|
+
self.frame_number += 1
|
164
|
+
img_np_array = await self.async_copy_array(self.img_base_layer)
|
233
165
|
_LOGGER.debug(
|
234
|
-
|
235
|
-
f" Auto cropping the image with rotation {int(self.shared.image_rotate)}"
|
166
|
+
"%s: Frame number %s", self.file_name, str(self.frame_number)
|
236
167
|
)
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
rand256=True,
|
168
|
+
if self.frame_number > 5:
|
169
|
+
self.frame_number = 0
|
170
|
+
|
171
|
+
# Draw map elements
|
172
|
+
img_np_array = await self._draw_map_elements(
|
173
|
+
img_np_array, m_json, colors, robot_position, robot_position_angle
|
244
174
|
)
|
175
|
+
|
176
|
+
# Final adjustments
|
245
177
|
pil_img = Image.fromarray(img_np_array, mode="RGBA")
|
246
178
|
del img_np_array # free memory
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
and self.shared.vacuum_state == "cleaning"
|
251
|
-
and self.zooming
|
252
|
-
and self.shared.image_zoom_lock_ratio
|
253
|
-
or self.shared.image_aspect_ratio != "None"
|
254
|
-
):
|
255
|
-
width = self.shared.image_ref_width
|
256
|
-
height = self.shared.image_ref_height
|
257
|
-
if self.shared.image_aspect_ratio != "None":
|
258
|
-
wsf, hsf = [
|
259
|
-
int(x) for x in self.shared.image_aspect_ratio.split(",")
|
260
|
-
]
|
261
|
-
_LOGGER.debug(f"Aspect Ratio: {wsf}, {hsf}")
|
262
|
-
if wsf == 0 or hsf == 0:
|
263
|
-
return pil_img
|
264
|
-
new_aspect_ratio = wsf / hsf
|
265
|
-
aspect_ratio = width / height
|
266
|
-
if aspect_ratio > new_aspect_ratio:
|
267
|
-
new_width = int(pil_img.height * new_aspect_ratio)
|
268
|
-
new_height = pil_img.height
|
269
|
-
else:
|
270
|
-
new_width = pil_img.width
|
271
|
-
new_height = int(pil_img.width / new_aspect_ratio)
|
272
|
-
|
273
|
-
resized = ImageOps.pad(pil_img, (new_width, new_height))
|
274
|
-
(
|
275
|
-
self.crop_img_size[0],
|
276
|
-
self.crop_img_size[1],
|
277
|
-
) = await self.async_map_coordinates_offset(
|
278
|
-
wsf, hsf, new_width, new_height
|
279
|
-
)
|
280
|
-
_LOGGER.debug(
|
281
|
-
f"{self.file_name}: Image Aspect Ratio ({wsf}, {hsf}): {new_width}x{new_height}"
|
282
|
-
)
|
283
|
-
_LOGGER.debug(f"{self.file_name}: Frame Completed.")
|
284
|
-
return resized
|
285
|
-
else:
|
286
|
-
_LOGGER.debug(f"{self.file_name}: Frame Completed.")
|
287
|
-
return ImageOps.pad(pil_img, (width, height))
|
288
|
-
else:
|
289
|
-
_LOGGER.debug(f"{self.file_name}: Frame Completed.")
|
290
|
-
return pil_img
|
179
|
+
|
180
|
+
return await self._finalize_image(pil_img)
|
181
|
+
|
291
182
|
except (RuntimeError, RuntimeWarning) as e:
|
292
183
|
_LOGGER.warning(
|
293
|
-
|
184
|
+
"%s: Runtime Error %s during image creation.",
|
185
|
+
self.file_name,
|
186
|
+
str(e),
|
294
187
|
exc_info=True,
|
295
188
|
)
|
296
189
|
return None
|
297
190
|
|
298
|
-
|
299
|
-
|
300
|
-
|
191
|
+
# If we reach here without returning, return None
|
192
|
+
return None
|
193
|
+
|
194
|
+
async def _setup_robot_and_image(
|
195
|
+
self, m_json, size_x, size_y, colors, destinations
|
196
|
+
):
|
197
|
+
(
|
198
|
+
_,
|
199
|
+
robot_position,
|
200
|
+
robot_position_angle,
|
201
|
+
) = await self.imd.async_get_robot_position(m_json)
|
202
|
+
|
203
|
+
if self.frame_number == 0:
|
204
|
+
# Create element map for tracking what's drawn where
|
205
|
+
self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
|
206
|
+
self.element_map[:] = DrawableElement.FLOOR
|
207
|
+
|
208
|
+
# Draw base layer if floor is enabled
|
209
|
+
if self.drawing_config.is_enabled(DrawableElement.FLOOR):
|
210
|
+
room_id, img_np_array = await self.imd.async_draw_base_layer(
|
211
|
+
m_json,
|
212
|
+
size_x,
|
213
|
+
size_y,
|
214
|
+
colors["wall"],
|
215
|
+
colors["zone_clean"],
|
216
|
+
colors["background"],
|
217
|
+
DEFAULT_PIXEL_SIZE,
|
218
|
+
)
|
219
|
+
_LOGGER.info("%s: Completed base Layers", self.file_name)
|
220
|
+
|
221
|
+
# Update element map for rooms
|
222
|
+
if 0 < room_id <= 15:
|
223
|
+
# This is a simplification - in a real implementation we would
|
224
|
+
# need to identify the exact pixels that belong to each room
|
225
|
+
pass
|
226
|
+
|
227
|
+
if room_id > 0 and not self.room_propriety:
|
228
|
+
self.room_propriety = await self.get_rooms_attributes(destinations)
|
229
|
+
if self.rooms_pos:
|
230
|
+
self.robot_pos = await self.async_get_robot_in_room(
|
231
|
+
(robot_position[0] * 10),
|
232
|
+
(robot_position[1] * 10),
|
233
|
+
robot_position_angle,
|
234
|
+
)
|
235
|
+
self.img_base_layer = await self.async_copy_array(img_np_array)
|
236
|
+
else:
|
237
|
+
# If floor is disabled, create an empty image
|
238
|
+
background_color = self.drawing_config.get_property(
|
239
|
+
DrawableElement.FLOOR, "color", colors["background"]
|
240
|
+
)
|
241
|
+
img_np_array = await self.draw.create_empty_image(
|
242
|
+
size_x, size_y, background_color
|
243
|
+
)
|
244
|
+
self.img_base_layer = await self.async_copy_array(img_np_array)
|
245
|
+
return self.img_base_layer, robot_position, robot_position_angle
|
246
|
+
|
247
|
+
async def _draw_map_elements(
|
248
|
+
self, img_np_array, m_json, colors, robot_position, robot_position_angle
|
249
|
+
):
|
250
|
+
# Draw charger if enabled
|
251
|
+
if self.drawing_config.is_enabled(DrawableElement.CHARGER):
|
252
|
+
img_np_array, self.charger_pos = await self.imd.async_draw_charger(
|
253
|
+
img_np_array, m_json, colors["charger"]
|
254
|
+
)
|
301
255
|
|
302
|
-
|
303
|
-
|
304
|
-
|
256
|
+
# Draw zones if enabled
|
257
|
+
if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
|
258
|
+
img_np_array = await self.imd.async_draw_zones(
|
259
|
+
m_json, img_np_array, colors["zone_clean"]
|
260
|
+
)
|
305
261
|
|
306
|
-
|
307
|
-
|
308
|
-
|
262
|
+
# Draw virtual restrictions if enabled
|
263
|
+
if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
|
264
|
+
img_np_array = await self.imd.async_draw_virtual_restrictions(
|
265
|
+
m_json, img_np_array, colors["no_go"]
|
266
|
+
)
|
309
267
|
|
310
|
-
|
311
|
-
|
312
|
-
|
268
|
+
# Draw path if enabled
|
269
|
+
if self.drawing_config.is_enabled(DrawableElement.PATH):
|
270
|
+
img_np_array = await self.imd.async_draw_path(
|
271
|
+
img_np_array, m_json, colors["move"]
|
272
|
+
)
|
273
|
+
|
274
|
+
# Draw go-to flag if enabled
|
275
|
+
if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
|
276
|
+
img_np_array = await self.imd.async_draw_go_to_flag(
|
277
|
+
img_np_array, m_json, colors["go_to"]
|
278
|
+
)
|
313
279
|
|
314
|
-
|
315
|
-
|
316
|
-
|
280
|
+
# Draw robot if enabled
|
281
|
+
if robot_position and self.drawing_config.is_enabled(DrawableElement.ROBOT):
|
282
|
+
# Get robot color (allows for customization)
|
283
|
+
robot_color = self.drawing_config.get_property(
|
284
|
+
DrawableElement.ROBOT, "color", colors["robot"]
|
285
|
+
)
|
286
|
+
|
287
|
+
img_np_array = await self.imd.async_draw_robot_on_map(
|
288
|
+
img_np_array, robot_position, robot_position_angle, robot_color
|
289
|
+
)
|
290
|
+
|
291
|
+
img_np_array = await self.async_auto_trim_and_zoom_image(
|
292
|
+
img_np_array,
|
293
|
+
detect_colour=colors["background"],
|
294
|
+
margin_size=int(self.shared.margins),
|
295
|
+
rotate=int(self.shared.image_rotate),
|
296
|
+
zoom=self.zooming,
|
297
|
+
rand256=True,
|
298
|
+
)
|
299
|
+
return img_np_array
|
300
|
+
|
301
|
+
async def _finalize_image(self, pil_img):
|
302
|
+
if not self.shared.image_ref_width or not self.shared.image_ref_height:
|
303
|
+
_LOGGER.warning(
|
304
|
+
"Image finalization failed: Invalid image dimensions. Returning original image."
|
305
|
+
)
|
306
|
+
return pil_img
|
307
|
+
if self.check_zoom_and_aspect_ratio():
|
308
|
+
resize_params = prepare_resize_params(self, pil_img, True)
|
309
|
+
pil_img = await self.async_resize_images(resize_params)
|
310
|
+
_LOGGER.debug("%s: Frame Completed.", self.file_name)
|
311
|
+
return pil_img
|
317
312
|
|
318
313
|
async def get_rooms_attributes(
|
319
314
|
self, destinations: JsonType = None
|
320
|
-
) -> RoomsProperties:
|
315
|
+
) -> tuple[RoomsProperties, Any, Any]:
|
321
316
|
"""Return the rooms attributes."""
|
322
317
|
if self.room_propriety:
|
323
318
|
return self.room_propriety
|
@@ -330,101 +325,221 @@ class ReImageHandler(object):
|
|
330
325
|
_LOGGER.debug("Got Rooms Attributes.")
|
331
326
|
return self.room_propriety
|
332
327
|
|
328
|
+
@staticmethod
|
329
|
+
def point_in_polygon(x: int, y: int, polygon: list) -> bool:
|
330
|
+
"""
|
331
|
+
Check if a point is inside a polygon using ray casting algorithm.
|
332
|
+
Enhanced version with better handling of edge cases.
|
333
|
+
|
334
|
+
Args:
|
335
|
+
x: X coordinate of the point
|
336
|
+
y: Y coordinate of the point
|
337
|
+
polygon: List of (x, y) tuples forming the polygon
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
True if the point is inside the polygon, False otherwise
|
341
|
+
"""
|
342
|
+
# Ensure we have a valid polygon with at least 3 points
|
343
|
+
if len(polygon) < 3:
|
344
|
+
return False
|
345
|
+
|
346
|
+
# Make sure the polygon is closed (last point equals first point)
|
347
|
+
if polygon[0] != polygon[-1]:
|
348
|
+
polygon = polygon + [polygon[0]]
|
349
|
+
|
350
|
+
# Use winding number algorithm for better accuracy
|
351
|
+
wn = 0 # Winding number counter
|
352
|
+
|
353
|
+
# Loop through all edges of the polygon
|
354
|
+
for i in range(len(polygon) - 1): # Last vertex is first vertex
|
355
|
+
p1x, p1y = polygon[i]
|
356
|
+
p2x, p2y = polygon[i + 1]
|
357
|
+
|
358
|
+
# Test if a point is left/right/on the edge defined by two vertices
|
359
|
+
if p1y <= y: # Start y <= P.y
|
360
|
+
if p2y > y: # End y > P.y (upward crossing)
|
361
|
+
# Point left of edge
|
362
|
+
if ((p2x - p1x) * (y - p1y) - (x - p1x) * (p2y - p1y)) > 0:
|
363
|
+
wn += 1 # Valid up intersect
|
364
|
+
else: # Start y > P.y
|
365
|
+
if p2y <= y: # End y <= P.y (downward crossing)
|
366
|
+
# Point right of edge
|
367
|
+
if ((p2x - p1x) * (y - p1y) - (x - p1x) * (p2y - p1y)) < 0:
|
368
|
+
wn -= 1 # Valid down intersect
|
369
|
+
|
370
|
+
# If winding number is not 0, the point is inside the polygon
|
371
|
+
return wn != 0
|
372
|
+
|
333
373
|
async def async_get_robot_in_room(
|
334
374
|
self, robot_x: int, robot_y: int, angle: float
|
335
375
|
) -> RobotPosition:
|
336
376
|
"""Get the robot position and return in what room is."""
|
377
|
+
# First check if we already have a cached room and if the robot is still in it
|
378
|
+
if self.robot_in_room:
|
379
|
+
# If we have outline data, use point_in_polygon for accurate detection
|
380
|
+
if "outline" in self.robot_in_room:
|
381
|
+
outline = self.robot_in_room["outline"]
|
382
|
+
if self.point_in_polygon(int(robot_x), int(robot_y), outline):
|
383
|
+
temp = {
|
384
|
+
"x": robot_x,
|
385
|
+
"y": robot_y,
|
386
|
+
"angle": angle,
|
387
|
+
"in_room": self.robot_in_room["room"],
|
388
|
+
}
|
389
|
+
# Handle active zones
|
390
|
+
self.active_zones = self.shared.rand256_active_zone
|
391
|
+
self.zooming = False
|
392
|
+
if self.active_zones and (
|
393
|
+
self.robot_in_room["id"]
|
394
|
+
in range(len(self.active_zones))
|
395
|
+
):
|
396
|
+
self.zooming = bool(
|
397
|
+
self.active_zones[self.robot_in_room["id"]]
|
398
|
+
)
|
399
|
+
else:
|
400
|
+
self.zooming = False
|
401
|
+
return temp
|
402
|
+
# Fallback to bounding box check if no outline data
|
403
|
+
elif all(
|
404
|
+
k in self.robot_in_room for k in ["left", "right", "up", "down"]
|
405
|
+
):
|
406
|
+
if (
|
407
|
+
(self.robot_in_room["right"] <= int(robot_x) <= self.robot_in_room["left"])
|
408
|
+
and (self.robot_in_room["up"] <= int(robot_y) <= self.robot_in_room["down"])
|
409
|
+
):
|
410
|
+
temp = {
|
411
|
+
"x": robot_x,
|
412
|
+
"y": robot_y,
|
413
|
+
"angle": angle,
|
414
|
+
"in_room": self.robot_in_room["room"],
|
415
|
+
}
|
416
|
+
# Handle active zones
|
417
|
+
self.active_zones = self.shared.rand256_active_zone
|
418
|
+
self.zooming = False
|
419
|
+
if self.active_zones and (
|
420
|
+
self.robot_in_room["id"]
|
421
|
+
in range(len(self.active_zones))
|
422
|
+
):
|
423
|
+
self.zooming = bool(
|
424
|
+
self.active_zones[self.robot_in_room["id"]]
|
425
|
+
)
|
426
|
+
else:
|
427
|
+
self.zooming = False
|
428
|
+
return temp
|
337
429
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
self.robot_in_room["down"] <= y
|
344
|
-
)
|
345
|
-
return x_in_room and y_in_room
|
430
|
+
# If we don't have a cached room or the robot is not in it, search all rooms
|
431
|
+
last_room = None
|
432
|
+
room_count = 0
|
433
|
+
if self.robot_in_room:
|
434
|
+
last_room = self.robot_in_room
|
346
435
|
|
347
|
-
# Check if the robot
|
348
|
-
|
436
|
+
# Check if the robot is far outside the normal map boundaries
|
437
|
+
# This helps prevent false positives for points very far from any room
|
438
|
+
map_boundary = 50000 # Typical map size is around 25000-30000 units for Rand25
|
439
|
+
if abs(robot_x) > map_boundary or abs(robot_y) > map_boundary:
|
440
|
+
_LOGGER.debug(
|
441
|
+
"%s robot position (%s, %s) is far outside map boundaries.",
|
442
|
+
self.file_name,
|
443
|
+
robot_x,
|
444
|
+
robot_y,
|
445
|
+
)
|
446
|
+
self.robot_in_room = last_room
|
447
|
+
self.zooming = False
|
349
448
|
temp = {
|
350
449
|
"x": robot_x,
|
351
450
|
"y": robot_y,
|
352
451
|
"angle": angle,
|
353
|
-
"in_room":
|
452
|
+
"in_room": last_room["room"] if last_room else "unknown",
|
354
453
|
}
|
355
|
-
|
454
|
+
return temp
|
455
|
+
|
456
|
+
# Search through all rooms to find which one contains the robot
|
457
|
+
if not self.rooms_pos:
|
458
|
+
_LOGGER.debug(
|
459
|
+
"%s: No rooms data available for robot position detection.",
|
460
|
+
self.file_name,
|
461
|
+
)
|
462
|
+
self.robot_in_room = last_room
|
356
463
|
self.zooming = False
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
464
|
+
temp = {
|
465
|
+
"x": robot_x,
|
466
|
+
"y": robot_y,
|
467
|
+
"angle": angle,
|
468
|
+
"in_room": last_room["room"] if last_room else "unknown",
|
469
|
+
}
|
361
470
|
return temp
|
362
|
-
|
363
|
-
_LOGGER.debug(
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
"up": corners[0][1],
|
377
|
-
"down": corners[2][1],
|
378
|
-
"room": room["name"],
|
379
|
-
}
|
380
|
-
# Check if the robot coordinates are inside the room's corners
|
381
|
-
if _check_robot_position(robot_x, robot_y):
|
471
|
+
|
472
|
+
_LOGGER.debug("%s: Searching for robot in rooms...", self.file_name)
|
473
|
+
for room in self.rooms_pos:
|
474
|
+
# Check if the room has an outline (polygon points)
|
475
|
+
if "outline" in room:
|
476
|
+
outline = room["outline"]
|
477
|
+
# Use point_in_polygon for accurate detection with complex shapes
|
478
|
+
if self.point_in_polygon(int(robot_x), int(robot_y), outline):
|
479
|
+
# Robot is in this room
|
480
|
+
self.robot_in_room = {
|
481
|
+
"id": room_count,
|
482
|
+
"room": str(room["name"]),
|
483
|
+
"outline": outline,
|
484
|
+
}
|
382
485
|
temp = {
|
383
486
|
"x": robot_x,
|
384
487
|
"y": robot_y,
|
385
488
|
"angle": angle,
|
386
489
|
"in_room": self.robot_in_room["room"],
|
387
490
|
}
|
491
|
+
|
492
|
+
# Handle active zones - Set zooming based on active zones
|
493
|
+
self.active_zones = self.shared.rand256_active_zone
|
494
|
+
self.zooming = False
|
495
|
+
if self.active_zones and (
|
496
|
+
self.robot_in_room["id"]
|
497
|
+
in range(len(self.active_zones))
|
498
|
+
):
|
499
|
+
self.zooming = bool(
|
500
|
+
self.active_zones[self.robot_in_room["id"]]
|
501
|
+
)
|
502
|
+
else:
|
503
|
+
self.zooming = False
|
504
|
+
|
388
505
|
_LOGGER.debug(
|
389
|
-
|
506
|
+
"%s is in %s room (polygon detection).",
|
507
|
+
self.file_name,
|
508
|
+
self.robot_in_room["room"],
|
390
509
|
)
|
391
|
-
del room, corners, robot_x, robot_y # free memory.
|
392
510
|
return temp
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
self.
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
511
|
+
room_count += 1
|
512
|
+
|
513
|
+
# Robot not found in any room
|
514
|
+
_LOGGER.debug(
|
515
|
+
"%s not located within any room coordinates.",
|
516
|
+
self.file_name,
|
517
|
+
)
|
518
|
+
self.robot_in_room = last_room
|
519
|
+
self.zooming = False
|
520
|
+
temp = {
|
521
|
+
"x": robot_x,
|
522
|
+
"y": robot_y,
|
523
|
+
"angle": angle,
|
524
|
+
"in_room": last_room["room"] if last_room else "unknown",
|
525
|
+
}
|
526
|
+
return temp
|
406
527
|
|
407
|
-
def get_calibration_data(self, rotation_angle: int = 0) ->
|
528
|
+
def get_calibration_data(self, rotation_angle: int = 0) -> Any:
|
408
529
|
"""Return the map calibration data."""
|
409
|
-
if not self.calibration_data:
|
530
|
+
if not self.calibration_data and self.crop_img_size:
|
410
531
|
self.calibration_data = []
|
411
532
|
_LOGGER.info(
|
412
|
-
|
533
|
+
"%s: Getting Calibrations points %s",
|
534
|
+
self.file_name,
|
535
|
+
str(self.crop_area),
|
413
536
|
)
|
414
537
|
|
415
538
|
# Define the map points (fixed)
|
416
|
-
map_points =
|
417
|
-
{"x": 0, "y": 0}, # Top-left corner 0
|
418
|
-
{"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
|
419
|
-
{
|
420
|
-
"x": self.crop_img_size[0],
|
421
|
-
"y": self.crop_img_size[1],
|
422
|
-
}, # Bottom-right corner 2
|
423
|
-
{"x": 0, "y": self.crop_img_size[1]}, # Bottom-left corner (optional) 3
|
424
|
-
]
|
539
|
+
map_points = self.get_map_points()
|
425
540
|
|
426
541
|
# Valetudo Re version need corrections of the coordinates and are implemented with *10
|
427
|
-
vacuum_points = self.
|
542
|
+
vacuum_points = self.re_get_vacuum_points(rotation_angle)
|
428
543
|
|
429
544
|
# Create the calibration data for each point
|
430
545
|
for vacuum_point, map_point in zip(vacuum_points, map_points):
|
@@ -433,23 +548,27 @@ class ReImageHandler(object):
|
|
433
548
|
|
434
549
|
return self.calibration_data
|
435
550
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
551
|
+
# Element selection methods
|
552
|
+
def enable_element(self, element_code: DrawableElement) -> None:
|
553
|
+
"""Enable drawing of a specific element."""
|
554
|
+
self.drawing_config.enable_element(element_code)
|
555
|
+
|
556
|
+
def disable_element(self, element_code: DrawableElement) -> None:
|
557
|
+
"""Disable drawing of a specific element."""
|
558
|
+
manage_drawable_elements(self, "disable", element_code=element_code)
|
559
|
+
|
560
|
+
def set_elements(self, element_codes: list[DrawableElement]) -> None:
|
561
|
+
"""Enable only the specified elements, disable all others."""
|
562
|
+
manage_drawable_elements(self, "set_elements", element_codes=element_codes)
|
442
563
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
self
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
self.imu.set_image_offset_ratio_16_9(width, height, rand256=True)
|
455
|
-
return width, height
|
564
|
+
def set_element_property(
|
565
|
+
self, element_code: DrawableElement, property_name: str, value
|
566
|
+
) -> None:
|
567
|
+
"""Set a drawing property for an element."""
|
568
|
+
manage_drawable_elements(
|
569
|
+
self,
|
570
|
+
"set_property",
|
571
|
+
element_code=element_code,
|
572
|
+
property_name=property_name,
|
573
|
+
value=value,
|
574
|
+
)
|