valetudo-map-parser 0.1.9b54__py3-none-any.whl → 0.1.9b56__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 +2 -1
- valetudo_map_parser/config/shared.py +1 -0
- valetudo_map_parser/hypfer_draw.py +7 -0
- valetudo_map_parser/rand25_handler.py +195 -128
- valetudo_map_parser/rooms_handler.py +246 -1
- {valetudo_map_parser-0.1.9b54.dist-info → valetudo_map_parser-0.1.9b56.dist-info}/METADATA +1 -1
- {valetudo_map_parser-0.1.9b54.dist-info → valetudo_map_parser-0.1.9b56.dist-info}/RECORD +10 -10
- {valetudo_map_parser-0.1.9b54.dist-info → valetudo_map_parser-0.1.9b56.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b54.dist-info → valetudo_map_parser-0.1.9b56.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b54.dist-info → valetudo_map_parser-0.1.9b56.dist-info}/WHEEL +0 -0
valetudo_map_parser/__init__.py
CHANGED
@@ -17,11 +17,12 @@ from .config.types import (
|
|
17
17
|
)
|
18
18
|
from .hypfer_handler import HypferMapImageHandler
|
19
19
|
from .rand25_handler import ReImageHandler
|
20
|
-
from .rooms_handler import RoomsHandler
|
20
|
+
from .rooms_handler import RoomsHandler, RandRoomsHandler
|
21
21
|
|
22
22
|
|
23
23
|
__all__ = [
|
24
24
|
"RoomsHandler",
|
25
|
+
"RandRoomsHandler",
|
25
26
|
"HypferMapImageHandler",
|
26
27
|
"ReImageHandler",
|
27
28
|
"RRMapParser",
|
@@ -88,6 +88,7 @@ class CameraShared:
|
|
88
88
|
self.vac_json_id = None # Vacuum json id
|
89
89
|
self.margins = "100" # Image margins
|
90
90
|
self.obstacles_data = None # Obstacles data
|
91
|
+
self.obstacles_pos = None # Obstacles position
|
91
92
|
self.offset_top = 0 # Image offset top
|
92
93
|
self.offset_down = 0 # Image offset down
|
93
94
|
self.offset_left = 0 # Image offset left
|
@@ -303,6 +303,13 @@ class ImageDraw:
|
|
303
303
|
await self.img_h.draw.async_draw_obstacles(
|
304
304
|
np_array, obstacle_positions, color_no_go
|
305
305
|
)
|
306
|
+
|
307
|
+
# Update both obstacles_pos and obstacles_data
|
308
|
+
self.img_h.shared.obstacles_pos = obstacle_positions
|
309
|
+
# Only update obstacles_data if it's None or if the number of obstacles has changed
|
310
|
+
if (self.img_h.shared.obstacles_data is None or
|
311
|
+
len(self.img_h.shared.obstacles_data) != len(obstacle_positions)):
|
312
|
+
self.img_h.shared.obstacles_data = obstacle_positions
|
306
313
|
return np_array
|
307
314
|
|
308
315
|
async def async_draw_charger(
|
@@ -29,13 +29,13 @@ from .config.types import (
|
|
29
29
|
)
|
30
30
|
from .config.utils import (
|
31
31
|
BaseHandler,
|
32
|
-
async_extract_room_outline,
|
33
32
|
initialize_drawing_config,
|
34
33
|
manage_drawable_elements,
|
35
34
|
prepare_resize_params,
|
36
35
|
)
|
37
36
|
from .map_data import RandImageData
|
38
37
|
from .reimg_draw import ImageDraw
|
38
|
+
from .rooms_handler import RandRoomsHandler
|
39
39
|
|
40
40
|
|
41
41
|
_LOGGER = logging.getLogger(__name__)
|
@@ -68,38 +68,13 @@ class ReImageHandler(BaseHandler, AutoCrop):
|
|
68
68
|
self.active_zones = None # Active zones
|
69
69
|
self.file_name = self.shared.file_name # File name
|
70
70
|
self.imd = ImageDraw(self) # Image Draw
|
71
|
-
|
72
|
-
async def extract_room_outline_from_map(self, room_id_int, pixels):
|
73
|
-
"""Extract the outline of a room using the pixel data and element map.
|
74
|
-
|
75
|
-
Args:
|
76
|
-
room_id_int: The room ID as an integer
|
77
|
-
pixels: List of pixel coordinates in the format [[x, y, z], ...]
|
78
|
-
|
79
|
-
Returns:
|
80
|
-
List of points forming the outline of the room
|
81
|
-
"""
|
82
|
-
# Calculate x and y min/max from compressed pixels for rectangular fallback
|
83
|
-
x_values = []
|
84
|
-
y_values = []
|
85
|
-
for x, y, _ in pixels:
|
86
|
-
x_values.append(x)
|
87
|
-
y_values.append(y)
|
88
|
-
|
89
|
-
if not x_values or not y_values:
|
90
|
-
return []
|
91
|
-
|
92
|
-
min_x, max_x = min(x_values), max(x_values)
|
93
|
-
min_y, max_y = min(y_values), max(y_values)
|
94
|
-
|
95
|
-
# Always return a rectangular outline since element_map is removed
|
96
|
-
return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
|
71
|
+
self.rooms_handler = RandRoomsHandler(self.file_name, self.drawing_config) # Room data handler
|
97
72
|
|
98
73
|
async def extract_room_properties(
|
99
74
|
self, json_data: JsonType, destinations: JsonType
|
100
75
|
) -> RoomsProperties:
|
101
76
|
"""Extract the room properties."""
|
102
|
-
unsorted_id = RandImageData.get_rrm_segments_ids(json_data)
|
77
|
+
# unsorted_id = RandImageData.get_rrm_segments_ids(json_data)
|
103
78
|
size_x, size_y = RandImageData.get_rrm_image_size(json_data)
|
104
79
|
top, left = RandImageData.get_rrm_image_position(json_data)
|
105
80
|
try:
|
@@ -110,62 +85,44 @@ class ReImageHandler(BaseHandler, AutoCrop):
|
|
110
85
|
) = await RandImageData.async_get_rrm_segments(
|
111
86
|
json_data, size_x, size_y, top, left, True
|
112
87
|
)
|
88
|
+
|
113
89
|
dest_json = destinations
|
114
|
-
room_data = dict(dest_json).get("rooms", [])
|
115
90
|
zones_data = dict(dest_json).get("zones", [])
|
116
91
|
points_data = dict(dest_json).get("spots", [])
|
117
|
-
|
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
|
118
99
|
self.rooms_pos = []
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
"name": name,
|
137
|
-
"corners": corners,
|
138
|
-
}
|
139
|
-
)
|
140
|
-
room_properties[int(room_id)] = {
|
141
|
-
"number": int(room_id),
|
142
|
-
"outline": corners,
|
143
|
-
"name": name,
|
144
|
-
"x": (x_min + x_max) // 2,
|
145
|
-
"y": (y_min + y_max) // 2,
|
146
|
-
}
|
147
|
-
# get the zones and points data
|
148
|
-
zone_properties = await self.async_zone_propriety(zones_data)
|
149
|
-
# get the points data
|
150
|
-
point_properties = await self.async_points_propriety(points_data)
|
151
|
-
if room_properties or zone_properties:
|
152
|
-
extracted_data = [
|
153
|
-
f"{len(room_properties)} Rooms" if room_properties else None,
|
154
|
-
f"{len(zone_properties)} Zones" if zone_properties else None,
|
155
|
-
]
|
156
|
-
extracted_data = ", ".join(filter(None, extracted_data))
|
157
|
-
_LOGGER.debug("Extracted data: %s", extracted_data)
|
158
|
-
else:
|
159
|
-
self.rooms_pos = None
|
160
|
-
_LOGGER.debug(
|
161
|
-
"%s: Rooms and Zones data not available!", self.file_name
|
162
|
-
)
|
163
|
-
rooms = RoomStore(self.file_name, room_properties)
|
164
|
-
_LOGGER.debug("Rooms Data: %s", rooms.get_rooms())
|
165
|
-
return room_properties, zone_properties, point_properties
|
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)
|
166
117
|
else:
|
167
|
-
|
168
|
-
|
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
|
169
126
|
except (RuntimeError, ValueError) as e:
|
170
127
|
_LOGGER.debug(
|
171
128
|
"No rooms Data or Error in extract_room_properties: %s",
|
@@ -368,79 +325,189 @@ class ReImageHandler(BaseHandler, AutoCrop):
|
|
368
325
|
_LOGGER.debug("Got Rooms Attributes.")
|
369
326
|
return self.room_propriety
|
370
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
|
+
|
371
373
|
async def async_get_robot_in_room(
|
372
374
|
self, robot_x: int, robot_y: int, angle: float
|
373
375
|
) -> RobotPosition:
|
374
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
|
375
429
|
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
)
|
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
|
382
435
|
|
383
|
-
#
|
384
|
-
|
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
|
385
448
|
temp = {
|
386
449
|
"x": robot_x,
|
387
450
|
"y": robot_y,
|
388
451
|
"angle": angle,
|
389
|
-
"in_room":
|
452
|
+
"in_room": last_room["room"] if last_room else "unknown",
|
390
453
|
}
|
391
|
-
self.active_zones = self.shared.rand256_active_zone
|
392
|
-
self.zooming = False
|
393
|
-
if self.active_zones and (
|
394
|
-
(self.robot_in_room["id"]) in range(len(self.active_zones))
|
395
|
-
): # issue #100 Index out of range
|
396
|
-
self.zooming = bool(self.active_zones[self.robot_in_room["id"]])
|
397
454
|
return temp
|
398
|
-
# else we need to search and use the async method
|
399
|
-
_LOGGER.debug("%s Changed room.. searching..", self.file_name)
|
400
|
-
room_count = -1
|
401
|
-
last_room = None
|
402
455
|
|
403
|
-
#
|
456
|
+
# Search through all rooms to find which one contains the robot
|
404
457
|
if not self.rooms_pos:
|
405
|
-
_LOGGER.debug(
|
406
|
-
|
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
|
463
|
+
self.zooming = False
|
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
|
+
}
|
470
|
+
return temp
|
407
471
|
|
408
|
-
|
409
|
-
if self.robot_in_room:
|
410
|
-
last_room = self.robot_in_room
|
472
|
+
_LOGGER.debug("%s: Searching for robot in rooms...", self.file_name)
|
411
473
|
for room in self.rooms_pos:
|
412
|
-
|
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
|
+
}
|
485
|
+
temp = {
|
486
|
+
"x": robot_x,
|
487
|
+
"y": robot_y,
|
488
|
+
"angle": angle,
|
489
|
+
"in_room": self.robot_in_room["room"],
|
490
|
+
}
|
491
|
+
_LOGGER.debug(
|
492
|
+
"%s is in %s room (polygon detection).",
|
493
|
+
self.file_name,
|
494
|
+
self.robot_in_room["room"],
|
495
|
+
)
|
496
|
+
return temp
|
413
497
|
room_count += 1
|
414
|
-
|
415
|
-
|
416
|
-
"left": corners[0][0],
|
417
|
-
"right": corners[2][0],
|
418
|
-
"up": corners[0][1],
|
419
|
-
"down": corners[2][1],
|
420
|
-
"room": room["name"],
|
421
|
-
}
|
422
|
-
# Check if the robot coordinates are inside the room's corners
|
423
|
-
if _check_robot_position(robot_x, robot_y):
|
424
|
-
temp = {
|
425
|
-
"x": robot_x,
|
426
|
-
"y": robot_y,
|
427
|
-
"angle": angle,
|
428
|
-
"in_room": self.robot_in_room["room"],
|
429
|
-
}
|
430
|
-
_LOGGER.debug("%s is in %s", self.file_name, self.robot_in_room["room"])
|
431
|
-
del room, corners, robot_x, robot_y # free memory.
|
432
|
-
return temp
|
433
|
-
# After checking all rooms and not finding a match
|
498
|
+
|
499
|
+
# Robot not found in any room
|
434
500
|
_LOGGER.debug(
|
435
|
-
"%s
|
501
|
+
"%s not located within any room coordinates.",
|
502
|
+
self.file_name,
|
436
503
|
)
|
437
|
-
self.zooming = False
|
438
504
|
self.robot_in_room = last_room
|
505
|
+
self.zooming = False
|
439
506
|
temp = {
|
440
507
|
"x": robot_x,
|
441
508
|
"y": robot_y,
|
442
509
|
"angle": angle,
|
443
|
-
"in_room":
|
510
|
+
"in_room": last_room["room"] if last_room else "unknown",
|
444
511
|
}
|
445
512
|
return temp
|
446
513
|
|
@@ -8,7 +8,7 @@ Version: 0.1.9
|
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
10
|
import time
|
11
|
-
from typing import Any, Dict, Optional, Tuple
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
12
12
|
|
13
13
|
import numpy as np
|
14
14
|
from scipy.ndimage import binary_dilation, binary_erosion
|
@@ -17,6 +17,7 @@ from scipy.spatial import ConvexHull
|
|
17
17
|
from .config.drawable_elements import DrawableElement, DrawingConfig
|
18
18
|
from .config.types import LOGGER, RoomsProperties
|
19
19
|
|
20
|
+
from .map_data import RandImageData, ImageData
|
20
21
|
|
21
22
|
class RoomsHandler:
|
22
23
|
"""
|
@@ -223,3 +224,247 @@ class RoomsHandler:
|
|
223
224
|
total_time = time.time() - start_total
|
224
225
|
LOGGER.debug("Room extraction Total time: %.3fs", total_time)
|
225
226
|
return room_properties
|
227
|
+
|
228
|
+
class RandRoomsHandler:
|
229
|
+
"""
|
230
|
+
Handler for extracting and managing room data from Rand25 vacuum maps.
|
231
|
+
|
232
|
+
This class provides methods to:
|
233
|
+
- Extract room outlines using the Convex Hull algorithm
|
234
|
+
- Process room properties from JSON data and destinations JSON
|
235
|
+
- Generate room masks and extract contours
|
236
|
+
|
237
|
+
All methods are async for better integration with the rest of the codebase.
|
238
|
+
"""
|
239
|
+
|
240
|
+
def __init__(self, vacuum_id: str, drawing_config: Optional[DrawingConfig] = None):
|
241
|
+
"""
|
242
|
+
Initialize the RandRoomsHandler.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
vacuum_id: Identifier for the vacuum
|
246
|
+
drawing_config: Configuration for which elements to draw (optional)
|
247
|
+
"""
|
248
|
+
self.vacuum_id = vacuum_id
|
249
|
+
self.drawing_config = drawing_config
|
250
|
+
self.current_json_data = None # Will store the current JSON data being processed
|
251
|
+
self.segment_data = None # Segment data
|
252
|
+
self.outlines = None # Outlines data
|
253
|
+
|
254
|
+
@staticmethod
|
255
|
+
def sublist(data: list, chunk_size: int) -> list:
|
256
|
+
"""Split a list into chunks of specified size."""
|
257
|
+
return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
|
258
|
+
|
259
|
+
@staticmethod
|
260
|
+
def convex_hull_outline(points: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
|
261
|
+
"""
|
262
|
+
Generate a convex hull outline from a set of points.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
points: List of (x, y) coordinate tuples
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
List of (x, y) tuples forming the convex hull outline
|
269
|
+
"""
|
270
|
+
if len(points) == 0:
|
271
|
+
return []
|
272
|
+
|
273
|
+
# Convert to numpy array for processing
|
274
|
+
points_array = np.array(points)
|
275
|
+
|
276
|
+
if len(points) < 3:
|
277
|
+
# Not enough points for a convex hull, return the points as is
|
278
|
+
return [(int(x), int(y)) for x, y in points_array]
|
279
|
+
|
280
|
+
try:
|
281
|
+
# Calculate the convex hull
|
282
|
+
hull = ConvexHull(points_array)
|
283
|
+
|
284
|
+
# Extract the vertices in order
|
285
|
+
hull_points = [
|
286
|
+
(int(points_array[vertex][0]), int(points_array[vertex][1]))
|
287
|
+
for vertex in hull.vertices
|
288
|
+
]
|
289
|
+
|
290
|
+
# Close the polygon by adding the first point at the end
|
291
|
+
if hull_points[0] != hull_points[-1]:
|
292
|
+
hull_points.append(hull_points[0])
|
293
|
+
|
294
|
+
return hull_points
|
295
|
+
|
296
|
+
except Exception as e:
|
297
|
+
LOGGER.warning(f"Error calculating convex hull: {e}")
|
298
|
+
|
299
|
+
# Fallback to bounding box if convex hull fails
|
300
|
+
x_min, y_min = np.min(points_array, axis=0)
|
301
|
+
x_max, y_max = np.max(points_array, axis=0)
|
302
|
+
|
303
|
+
return [
|
304
|
+
(int(x_min), int(y_min)), # Top-left
|
305
|
+
(int(x_max), int(y_min)), # Top-right
|
306
|
+
(int(x_max), int(y_max)), # Bottom-right
|
307
|
+
(int(x_min), int(y_max)), # Bottom-left
|
308
|
+
(int(x_min), int(y_min)), # Back to top-left to close the polygon
|
309
|
+
]
|
310
|
+
|
311
|
+
async def _process_segment_data(
|
312
|
+
self, segment_data: List, segment_id: int, pixel_size: int
|
313
|
+
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
|
314
|
+
"""
|
315
|
+
Process a single segment and extract its outline.
|
316
|
+
|
317
|
+
Args:
|
318
|
+
segment_data: The segment pixel data
|
319
|
+
segment_id: The ID of the segment
|
320
|
+
pixel_size: The size of each pixel
|
321
|
+
|
322
|
+
Returns:
|
323
|
+
Tuple of (room_id, room_data) or (None, None) if processing failed
|
324
|
+
"""
|
325
|
+
# Check if this room is enabled in the drawing configuration
|
326
|
+
if self.drawing_config is not None:
|
327
|
+
try:
|
328
|
+
# Convert segment_id to room element (ROOM_1 to ROOM_15)
|
329
|
+
room_element_id = int(segment_id)
|
330
|
+
if 1 <= room_element_id <= 15:
|
331
|
+
room_element = getattr(
|
332
|
+
DrawableElement, f"ROOM_{room_element_id}", None
|
333
|
+
)
|
334
|
+
if room_element:
|
335
|
+
is_enabled = self.drawing_config.is_enabled(room_element)
|
336
|
+
if not is_enabled:
|
337
|
+
# Skip this room if it's disabled
|
338
|
+
LOGGER.debug("Skipping disabled room %s", segment_id)
|
339
|
+
return None, None
|
340
|
+
except (ValueError, TypeError):
|
341
|
+
# If segment_id is not a valid integer, we can't map it to a room element
|
342
|
+
# In this case, we'll include the room (fail open)
|
343
|
+
LOGGER.debug(
|
344
|
+
"Could not convert segment_id %s to room element", segment_id
|
345
|
+
)
|
346
|
+
|
347
|
+
# Skip if no pixels
|
348
|
+
if not segment_data:
|
349
|
+
return None, None
|
350
|
+
|
351
|
+
# Extract points from segment data
|
352
|
+
points = []
|
353
|
+
for x, y, _ in segment_data:
|
354
|
+
points.append((int(x), int(y)))
|
355
|
+
|
356
|
+
if not points:
|
357
|
+
return None, None
|
358
|
+
|
359
|
+
# Use convex hull to get the outline
|
360
|
+
outline = self.convex_hull_outline(points)
|
361
|
+
if not outline:
|
362
|
+
return None, None
|
363
|
+
|
364
|
+
# Calculate bounding box for the room
|
365
|
+
xs, ys = zip(*outline)
|
366
|
+
x_min, x_max = min(xs), max(xs)
|
367
|
+
y_min, y_max = min(ys), max(ys)
|
368
|
+
|
369
|
+
# Scale coordinates by pixel_size
|
370
|
+
scaled_outline = [
|
371
|
+
(int(x * pixel_size), int(y * pixel_size)) for x, y in outline
|
372
|
+
]
|
373
|
+
|
374
|
+
room_id = str(segment_id)
|
375
|
+
room_data = {
|
376
|
+
"number": segment_id,
|
377
|
+
"outline": scaled_outline,
|
378
|
+
"name": f"Room {segment_id}", # Default name, will be updated from destinations
|
379
|
+
"x": int(((x_min + x_max) * pixel_size) // 2),
|
380
|
+
"y": int(((y_min + y_max) * pixel_size) // 2),
|
381
|
+
}
|
382
|
+
|
383
|
+
return room_id, room_data
|
384
|
+
|
385
|
+
async def async_extract_room_properties(
|
386
|
+
self, json_data: Dict[str, Any], destinations: Dict[str, Any]
|
387
|
+
) -> RoomsProperties:
|
388
|
+
"""
|
389
|
+
Extract room properties from the JSON data and destinations.
|
390
|
+
|
391
|
+
Args:
|
392
|
+
json_data: The JSON data from the vacuum
|
393
|
+
destinations: The destinations JSON containing room names and IDs
|
394
|
+
|
395
|
+
Returns:
|
396
|
+
Dictionary of room properties
|
397
|
+
"""
|
398
|
+
start_total = time.time()
|
399
|
+
room_properties = {}
|
400
|
+
|
401
|
+
# Get basic map information
|
402
|
+
unsorted_id = RandImageData.get_rrm_segments_ids(json_data)
|
403
|
+
size_x, size_y = RandImageData.get_rrm_image_size(json_data)
|
404
|
+
top, left = RandImageData.get_rrm_image_position(json_data)
|
405
|
+
pixel_size = 50 # Rand25 vacuums use a larger pixel size to match the original implementation
|
406
|
+
|
407
|
+
# Get segment data and outlines if not already available
|
408
|
+
if not self.segment_data or not self.outlines:
|
409
|
+
(
|
410
|
+
self.segment_data,
|
411
|
+
self.outlines,
|
412
|
+
) = await RandImageData.async_get_rrm_segments(
|
413
|
+
json_data, size_x, size_y, top, left, True
|
414
|
+
)
|
415
|
+
|
416
|
+
# Process destinations JSON to get room names
|
417
|
+
dest_json = destinations
|
418
|
+
room_data = dest_json.get("rooms", [])
|
419
|
+
room_id_to_data = {room["id"]: room for room in room_data}
|
420
|
+
|
421
|
+
# Process each segment
|
422
|
+
if unsorted_id and self.segment_data and self.outlines:
|
423
|
+
for idx, segment_id in enumerate(unsorted_id):
|
424
|
+
# Extract points from segment data
|
425
|
+
points = []
|
426
|
+
for x, y, _ in self.segment_data[idx]:
|
427
|
+
points.append((int(x), int(y)))
|
428
|
+
|
429
|
+
if not points:
|
430
|
+
continue
|
431
|
+
|
432
|
+
# Use convex hull to get the outline
|
433
|
+
outline = self.convex_hull_outline(points)
|
434
|
+
if not outline:
|
435
|
+
continue
|
436
|
+
|
437
|
+
# Scale coordinates by pixel_size
|
438
|
+
scaled_outline = [
|
439
|
+
(int(x * pixel_size), int(y * pixel_size)) for x, y in outline
|
440
|
+
]
|
441
|
+
|
442
|
+
# Calculate center point
|
443
|
+
xs, ys = zip(*outline)
|
444
|
+
x_min, x_max = min(xs), max(xs)
|
445
|
+
y_min, y_max = min(ys), max(ys)
|
446
|
+
center_x = int(((x_min + x_max) * pixel_size) // 2)
|
447
|
+
center_y = int(((y_min + y_max) * pixel_size) // 2)
|
448
|
+
|
449
|
+
# Create room data
|
450
|
+
room_id = str(segment_id)
|
451
|
+
room_data = {
|
452
|
+
"number": segment_id,
|
453
|
+
"outline": scaled_outline,
|
454
|
+
"name": f"Room {segment_id}", # Default name, will be updated from destinations
|
455
|
+
"x": center_x,
|
456
|
+
"y": center_y,
|
457
|
+
}
|
458
|
+
|
459
|
+
# Update room name from destinations if available
|
460
|
+
if segment_id in room_id_to_data:
|
461
|
+
room_info = room_id_to_data[segment_id]
|
462
|
+
room_data["name"] = room_info.get("name", room_data["name"])
|
463
|
+
|
464
|
+
room_properties[room_id] = room_data
|
465
|
+
|
466
|
+
# Log timing information
|
467
|
+
total_time = time.time() - start_total
|
468
|
+
LOGGER.debug("Room extraction Total time: %.3fs", total_time)
|
469
|
+
|
470
|
+
return room_properties
|
@@ -1,4 +1,4 @@
|
|
1
|
-
valetudo_map_parser/__init__.py,sha256=
|
1
|
+
valetudo_map_parser/__init__.py,sha256=Fz-gtKf_OlZcDQqVfGlBwIWi5DJAiRucMbBMdQ2tX_U,1060
|
2
2
|
valetudo_map_parser/config/__init__.py,sha256=DQ9plV3ZF_K25Dp5ZQHPDoG-40dQoJNdNi-dfNeR3Zc,48
|
3
3
|
valetudo_map_parser/config/auto_crop.py,sha256=6xt_wJQqphddWhlrr7MNUkodCi8ZYdRk42qvAaxlYCM,13546
|
4
4
|
valetudo_map_parser/config/color_utils.py,sha256=nXD6WeNmdFdoMxPDW-JFpjnxJSaZR1jX-ouNfrx6zvE,4502
|
@@ -9,19 +9,19 @@ valetudo_map_parser/config/enhanced_drawable.py,sha256=QlGxlUMVgECUXPtFwIslyjubW
|
|
9
9
|
valetudo_map_parser/config/optimized_element_map.py,sha256=52BCnkvVv9bre52LeVIfT8nhnEIpc0TuWTv1xcNu0Rk,15744
|
10
10
|
valetudo_map_parser/config/rand25_parser.py,sha256=kIayyqVZBfQfAMkiArzqrrj9vqZB3pkgT0Y5ufrQmGA,16448
|
11
11
|
valetudo_map_parser/config/room_outline.py,sha256=D20D-yeyKnlmVbW9lI7bsPtQGn2XkcWow6YNOEPnWVg,4800
|
12
|
-
valetudo_map_parser/config/shared.py,sha256=
|
12
|
+
valetudo_map_parser/config/shared.py,sha256=Vr4bicL7aJoRQbwbXyjEpiWhfzZ-cakLlfRqL3LBhpM,10475
|
13
13
|
valetudo_map_parser/config/types.py,sha256=TaRKoo7G7WIUw7ljOz2Vn5oYzKaLyQH-7Eb8ZYql8Ls,17464
|
14
14
|
valetudo_map_parser/config/utils.py,sha256=CFuuiS5IufEu9aeaZwi7xa1jEF1z6yDZB0mcyVX79Xo,29261
|
15
|
-
valetudo_map_parser/hypfer_draw.py,sha256=
|
15
|
+
valetudo_map_parser/hypfer_draw.py,sha256=bwNTYopTJFY0nElrHquQrSfGHgN_-6t5E-8xBxDsRXA,26660
|
16
16
|
valetudo_map_parser/hypfer_handler.py,sha256=wvkZt6MsUF0gkHZDAwiUgOOawkdvakRzYBV3NtjxuJQ,19938
|
17
17
|
valetudo_map_parser/hypfer_rooms_handler.py,sha256=NkpOA6Gdq-2D3lLAxvtNuuWMvPXHxeMY2TO5RZLSHlU,22652
|
18
18
|
valetudo_map_parser/map_data.py,sha256=zQKE8EzWxR0r0qyfD1QQq51T1wFrpcIeXtnpm92-LXQ,17743
|
19
19
|
valetudo_map_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
-
valetudo_map_parser/rand25_handler.py,sha256=
|
20
|
+
valetudo_map_parser/rand25_handler.py,sha256=GpY9R9EGWM06KYF4VQ1NxYw0idaJ8lZNkkA5wa0cr-c,22292
|
21
21
|
valetudo_map_parser/reimg_draw.py,sha256=1q8LkNTPHEA9Tsapc_JnVw51kpPYNhaBU-KmHkefCQY,12507
|
22
|
-
valetudo_map_parser/rooms_handler.py,sha256=
|
23
|
-
valetudo_map_parser-0.1.
|
24
|
-
valetudo_map_parser-0.1.
|
25
|
-
valetudo_map_parser-0.1.
|
26
|
-
valetudo_map_parser-0.1.
|
27
|
-
valetudo_map_parser-0.1.
|
22
|
+
valetudo_map_parser/rooms_handler.py,sha256=YP8OLotBH-RXluv398l7TTT2zIBHJp91b8THWxl3NdI,17794
|
23
|
+
valetudo_map_parser-0.1.9b56.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
|
24
|
+
valetudo_map_parser-0.1.9b56.dist-info/METADATA,sha256=LJXXb676sSJ9W7NGafpcJSF4CRGGhbg_lCyrzcxkge0,3321
|
25
|
+
valetudo_map_parser-0.1.9b56.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
|
26
|
+
valetudo_map_parser-0.1.9b56.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
27
|
+
valetudo_map_parser-0.1.9b56.dist-info/RECORD,,
|
File without changes
|
{valetudo_map_parser-0.1.9b54.dist-info → valetudo_map_parser-0.1.9b56.dist-info}/NOTICE.txt
RENAMED
File without changes
|
File without changes
|