valetudo-map-parser 0.1.9b51__tar.gz → 0.1.9b53__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/PKG-INFO +1 -1
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/colors.py +14 -4
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/drawable.py +0 -3
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/enhanced_drawable.py +2 -2
- valetudo_map_parser-0.1.9b53/SCR/valetudo_map_parser/hypfer_rooms_handler.py +599 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/pyproject.toml +1 -1
- valetudo_map_parser-0.1.9b51/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -406
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/README.md +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/rand25_parser.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/room_outline.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/shared.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/types.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/config/utils.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/hypfer_draw.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/hypfer_handler.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/map_data.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/py.typed +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/rand25_handler.py +0 -0
- {valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
@@ -492,10 +492,20 @@ class ColorsManagement:
|
|
492
492
|
background = tuple(map(int, array[y, x]))
|
493
493
|
|
494
494
|
# Update cache (with simple LRU-like behavior)
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
495
|
+
try:
|
496
|
+
if len(cache) >= ColorsManagement._cache_size:
|
497
|
+
# Remove a random entry if cache is full
|
498
|
+
if cache: # Make sure cache is not empty
|
499
|
+
cache.pop(next(iter(cache)))
|
500
|
+
else:
|
501
|
+
# If cache is somehow empty but len reported >= _cache_size
|
502
|
+
# This is an edge case that shouldn't happen but we handle it
|
503
|
+
pass
|
504
|
+
cache[cache_key] = background
|
505
|
+
except KeyError:
|
506
|
+
# If we encounter a KeyError, reset the cache
|
507
|
+
# This is a rare edge case that might happen in concurrent access
|
508
|
+
ColorsManagement._bg_color_cache = {cache_key: background}
|
499
509
|
|
500
510
|
except (IndexError, ValueError):
|
501
511
|
return foreground
|
@@ -57,9 +57,6 @@ class Drawable:
|
|
57
57
|
# Extract alpha from color
|
58
58
|
alpha = color[3] if len(color) == 4 else 255
|
59
59
|
|
60
|
-
# For debugging
|
61
|
-
_LOGGER.debug("Drawing with color %s and alpha %s", color, alpha)
|
62
|
-
|
63
60
|
# Create the full color with alpha
|
64
61
|
full_color = color if len(color) == 4 else (*color, 255)
|
65
62
|
|
@@ -14,11 +14,11 @@ from typing import Optional, Tuple
|
|
14
14
|
import numpy as np
|
15
15
|
|
16
16
|
from .drawable import Drawable
|
17
|
-
from
|
17
|
+
from .drawable_elements import (
|
18
18
|
DrawableElement,
|
19
19
|
DrawingConfig,
|
20
20
|
)
|
21
|
-
from
|
21
|
+
from .colors import ColorsManagement
|
22
22
|
|
23
23
|
|
24
24
|
# Type aliases
|
@@ -0,0 +1,599 @@
|
|
1
|
+
"""
|
2
|
+
Hipfer Rooms Handler Module.
|
3
|
+
Handles room data extraction and processing for Valetudo Hipfer vacuum maps.
|
4
|
+
Provides async methods for room outline extraction and properties management.
|
5
|
+
Version: 0.1.9
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from math import sqrt
|
11
|
+
from typing import Any, Dict, Optional, List, Tuple
|
12
|
+
|
13
|
+
import numpy as np
|
14
|
+
|
15
|
+
from .config.drawable_elements import DrawableElement, DrawingConfig
|
16
|
+
from .config.types import LOGGER, RoomsProperties, RoomStore
|
17
|
+
|
18
|
+
|
19
|
+
class HypferRoomsHandler:
|
20
|
+
"""
|
21
|
+
Handler for extracting and managing room data from Hipfer vacuum maps.
|
22
|
+
|
23
|
+
This class provides methods to:
|
24
|
+
- Extract room outlines using the Ramer-Douglas-Peucker algorithm
|
25
|
+
- Process room properties from JSON data
|
26
|
+
- Generate room masks and extract contours
|
27
|
+
|
28
|
+
All methods are async for better integration with the rest of the codebase.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, vacuum_id: str, drawing_config: Optional[DrawingConfig] = None):
|
32
|
+
"""
|
33
|
+
Initialize the HipferRoomsHandler.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
vacuum_id: Identifier for the vacuum
|
37
|
+
drawing_config: Configuration for which elements to draw (optional)
|
38
|
+
"""
|
39
|
+
self.vacuum_id = vacuum_id
|
40
|
+
self.drawing_config = drawing_config
|
41
|
+
self.current_json_data = None # Will store the current JSON data being processed
|
42
|
+
|
43
|
+
@staticmethod
|
44
|
+
def sublist(data: list, chunk_size: int) -> list:
|
45
|
+
return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
|
46
|
+
|
47
|
+
# Cache for RDP results
|
48
|
+
_rdp_cache = {}
|
49
|
+
|
50
|
+
@staticmethod
|
51
|
+
def perpendicular_distance(
|
52
|
+
point: tuple[int, int], line_start: tuple[int, int], line_end: tuple[int, int]
|
53
|
+
) -> float:
|
54
|
+
"""Calculate the perpendicular distance from a point to a line.
|
55
|
+
Optimized for performance.
|
56
|
+
"""
|
57
|
+
# Fast path for point-to-point distance
|
58
|
+
if line_start == line_end:
|
59
|
+
dx = point[0] - line_start[0]
|
60
|
+
dy = point[1] - line_start[1]
|
61
|
+
return sqrt(dx*dx + dy*dy)
|
62
|
+
|
63
|
+
x, y = point
|
64
|
+
x1, y1 = line_start
|
65
|
+
x2, y2 = line_end
|
66
|
+
|
67
|
+
# Precompute differences for efficiency
|
68
|
+
dx = x2 - x1
|
69
|
+
dy = y2 - y1
|
70
|
+
|
71
|
+
# Calculate the line length squared (avoid sqrt until needed)
|
72
|
+
line_length_sq = dx*dx + dy*dy
|
73
|
+
if line_length_sq == 0:
|
74
|
+
return 0
|
75
|
+
|
76
|
+
# Calculate the distance from the point to the line
|
77
|
+
# Using the formula: |cross_product| / |line_vector|
|
78
|
+
# This is more efficient than the original formula
|
79
|
+
cross_product = abs(dy * x - dx * y + x2 * y1 - y2 * x1)
|
80
|
+
return cross_product / sqrt(line_length_sq)
|
81
|
+
|
82
|
+
async def rdp(
|
83
|
+
self, points: List[Tuple[int, int]], epsilon: float
|
84
|
+
) -> List[Tuple[int, int]]:
|
85
|
+
"""Ramer-Douglas-Peucker algorithm for simplifying a curve.
|
86
|
+
Optimized with caching and better performance.
|
87
|
+
"""
|
88
|
+
# Create a hashable key for caching
|
89
|
+
# Convert points to a tuple for hashing
|
90
|
+
points_tuple = tuple(points)
|
91
|
+
cache_key = (points_tuple, epsilon)
|
92
|
+
|
93
|
+
# Check cache first
|
94
|
+
if cache_key in self._rdp_cache:
|
95
|
+
return self._rdp_cache[cache_key]
|
96
|
+
|
97
|
+
# Base case
|
98
|
+
if len(points) <= 2:
|
99
|
+
return points
|
100
|
+
|
101
|
+
# For very small point sets, process directly without recursion
|
102
|
+
if len(points) <= 5:
|
103
|
+
# Find the point with the maximum distance
|
104
|
+
dmax = 0
|
105
|
+
index = 0
|
106
|
+
for i in range(1, len(points) - 1):
|
107
|
+
d = self.perpendicular_distance(points[i], points[0], points[-1])
|
108
|
+
if d > dmax:
|
109
|
+
index = i
|
110
|
+
dmax = d
|
111
|
+
|
112
|
+
# If max distance is greater than epsilon, keep the point
|
113
|
+
if dmax > epsilon:
|
114
|
+
result = [points[0]] + [points[index]] + [points[-1]]
|
115
|
+
else:
|
116
|
+
result = [points[0], points[-1]]
|
117
|
+
|
118
|
+
# Cache and return
|
119
|
+
self._rdp_cache[cache_key] = result
|
120
|
+
return result
|
121
|
+
|
122
|
+
# For larger point sets, use numpy for faster distance calculation
|
123
|
+
if len(points) > 20:
|
124
|
+
# Convert to numpy arrays for vectorized operations
|
125
|
+
points_array = np.array(points)
|
126
|
+
start = points_array[0]
|
127
|
+
end = points_array[-1]
|
128
|
+
|
129
|
+
# Calculate perpendicular distances in one vectorized operation
|
130
|
+
line_vector = end - start
|
131
|
+
line_length = np.linalg.norm(line_vector)
|
132
|
+
|
133
|
+
if line_length == 0:
|
134
|
+
# If start and end are the same, use direct distance
|
135
|
+
distances = np.linalg.norm(points_array[1:-1] - start, axis=1)
|
136
|
+
else:
|
137
|
+
# Normalize line vector
|
138
|
+
line_vector = line_vector / line_length
|
139
|
+
# Calculate perpendicular distances using vector operations
|
140
|
+
vectors_to_points = points_array[1:-1] - start
|
141
|
+
# Project vectors onto line vector
|
142
|
+
projections = np.dot(vectors_to_points, line_vector)
|
143
|
+
# Calculate projected points on line
|
144
|
+
projected_points = start + np.outer(projections, line_vector)
|
145
|
+
# Calculate distances from points to their projections
|
146
|
+
distances = np.linalg.norm(points_array[1:-1] - projected_points, axis=1)
|
147
|
+
|
148
|
+
# Find the point with maximum distance
|
149
|
+
if len(distances) > 0:
|
150
|
+
max_idx = np.argmax(distances)
|
151
|
+
dmax = distances[max_idx]
|
152
|
+
index = max_idx + 1 # +1 because we skipped the first point
|
153
|
+
else:
|
154
|
+
dmax = 0
|
155
|
+
index = 0
|
156
|
+
else:
|
157
|
+
# For medium-sized point sets, use the original algorithm
|
158
|
+
dmax = 0
|
159
|
+
index = 0
|
160
|
+
for i in range(1, len(points) - 1):
|
161
|
+
d = self.perpendicular_distance(points[i], points[0], points[-1])
|
162
|
+
if d > dmax:
|
163
|
+
index = i
|
164
|
+
dmax = d
|
165
|
+
|
166
|
+
# If max distance is greater than epsilon, recursively simplify
|
167
|
+
if dmax > epsilon:
|
168
|
+
# Recursive call
|
169
|
+
first_segment = await self.rdp(points[: index + 1], epsilon)
|
170
|
+
second_segment = await self.rdp(points[index:], epsilon)
|
171
|
+
|
172
|
+
# Build the result list (avoiding duplicating the common point)
|
173
|
+
result = first_segment[:-1] + second_segment
|
174
|
+
else:
|
175
|
+
result = [points[0], points[-1]]
|
176
|
+
|
177
|
+
# Limit cache size
|
178
|
+
if len(self._rdp_cache) > 100: # Keep only 100 most recent items
|
179
|
+
try:
|
180
|
+
self._rdp_cache.pop(next(iter(self._rdp_cache)))
|
181
|
+
except (StopIteration, KeyError):
|
182
|
+
pass
|
183
|
+
|
184
|
+
# Cache the result
|
185
|
+
self._rdp_cache[cache_key] = result
|
186
|
+
return result
|
187
|
+
|
188
|
+
# Cache for corner results
|
189
|
+
_corners_cache = {}
|
190
|
+
|
191
|
+
async def async_get_corners(
|
192
|
+
self, mask: np.ndarray, epsilon_factor: float = 0.05
|
193
|
+
) -> List[Tuple[int, int]]:
|
194
|
+
"""
|
195
|
+
Get the corners of a room shape as a list of (x, y) tuples.
|
196
|
+
Uses contour detection and Douglas-Peucker algorithm to simplify the contour.
|
197
|
+
Optimized with caching and faster calculations.
|
198
|
+
|
199
|
+
Args:
|
200
|
+
mask: Binary mask of the room (1 for room, 0 for background)
|
201
|
+
epsilon_factor: Controls the level of simplification (higher = fewer points)
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
List of (x, y) tuples representing the corners of the room
|
205
|
+
"""
|
206
|
+
# Create a hash of the mask and epsilon factor for caching
|
207
|
+
mask_hash = hash((mask.tobytes(), epsilon_factor))
|
208
|
+
|
209
|
+
# Check if we have a cached result
|
210
|
+
if mask_hash in self._corners_cache:
|
211
|
+
return self._corners_cache[mask_hash]
|
212
|
+
|
213
|
+
# Fast path for empty masks
|
214
|
+
if not np.any(mask):
|
215
|
+
return []
|
216
|
+
|
217
|
+
# Find contours in the mask - this uses our optimized method with caching
|
218
|
+
contour = await self.async_moore_neighbor_trace(mask)
|
219
|
+
|
220
|
+
if not contour:
|
221
|
+
# Fallback to bounding box if contour detection fails
|
222
|
+
y_indices, x_indices = np.where(mask > 0)
|
223
|
+
if len(x_indices) == 0 or len(y_indices) == 0:
|
224
|
+
return []
|
225
|
+
|
226
|
+
x_min, x_max = np.min(x_indices), np.max(x_indices)
|
227
|
+
y_min, y_max = np.min(y_indices), np.max(y_indices)
|
228
|
+
|
229
|
+
result = [
|
230
|
+
(x_min, y_min), # Top-left
|
231
|
+
(x_max, y_min), # Top-right
|
232
|
+
(x_max, y_max), # Bottom-right
|
233
|
+
(x_min, y_max), # Bottom-left
|
234
|
+
(x_min, y_min), # Back to top-left to close the polygon
|
235
|
+
]
|
236
|
+
|
237
|
+
# Cache the result
|
238
|
+
self._corners_cache[mask_hash] = result
|
239
|
+
return result
|
240
|
+
|
241
|
+
# For small contours (less than 10 points), skip simplification
|
242
|
+
if len(contour) <= 10:
|
243
|
+
# Ensure the contour is closed
|
244
|
+
if contour[0] != contour[-1]:
|
245
|
+
contour.append(contour[0])
|
246
|
+
|
247
|
+
# Cache and return
|
248
|
+
self._corners_cache[mask_hash] = contour
|
249
|
+
return contour
|
250
|
+
|
251
|
+
# For larger contours, calculate perimeter more efficiently using numpy
|
252
|
+
points = np.array(contour)
|
253
|
+
# Calculate differences between consecutive points
|
254
|
+
diffs = np.diff(points, axis=0)
|
255
|
+
# Calculate squared distances
|
256
|
+
squared_dists = np.sum(diffs**2, axis=1)
|
257
|
+
# Calculate perimeter as sum of distances
|
258
|
+
perimeter = np.sum(np.sqrt(squared_dists))
|
259
|
+
|
260
|
+
# Apply Douglas-Peucker algorithm to simplify the contour
|
261
|
+
epsilon = epsilon_factor * perimeter
|
262
|
+
simplified_contour = await self.rdp(contour, epsilon=epsilon)
|
263
|
+
|
264
|
+
# Ensure the contour has at least 3 points to form a polygon
|
265
|
+
if len(simplified_contour) < 3:
|
266
|
+
# Fallback to bounding box
|
267
|
+
y_indices, x_indices = np.where(mask > 0)
|
268
|
+
x_min, x_max = int(np.min(x_indices)), int(np.max(x_indices))
|
269
|
+
y_min, y_max = int(np.min(y_indices)), int(np.max(y_indices))
|
270
|
+
|
271
|
+
LOGGER.debug(
|
272
|
+
f"{self.vacuum_id}: Too few points in contour, using bounding box"
|
273
|
+
)
|
274
|
+
result = [
|
275
|
+
(x_min, y_min), # Top-left
|
276
|
+
(x_max, y_min), # Top-right
|
277
|
+
(x_max, y_max), # Bottom-right
|
278
|
+
(x_min, y_max), # Bottom-left
|
279
|
+
(x_min, y_min), # Back to top-left to close the polygon
|
280
|
+
]
|
281
|
+
|
282
|
+
# Cache the result
|
283
|
+
self._corners_cache[mask_hash] = result
|
284
|
+
return result
|
285
|
+
|
286
|
+
# Ensure the contour is closed
|
287
|
+
if simplified_contour[0] != simplified_contour[-1]:
|
288
|
+
simplified_contour.append(simplified_contour[0])
|
289
|
+
|
290
|
+
# Limit cache size
|
291
|
+
if len(self._corners_cache) > 50: # Keep only 50 most recent items
|
292
|
+
try:
|
293
|
+
self._corners_cache.pop(next(iter(self._corners_cache)))
|
294
|
+
except (StopIteration, KeyError):
|
295
|
+
pass
|
296
|
+
|
297
|
+
# Cache the result
|
298
|
+
self._corners_cache[mask_hash] = simplified_contour
|
299
|
+
return simplified_contour
|
300
|
+
|
301
|
+
# Cache for labeled arrays to avoid redundant calculations
|
302
|
+
_label_cache = {}
|
303
|
+
_hull_cache = {}
|
304
|
+
|
305
|
+
@staticmethod
|
306
|
+
async def async_moore_neighbor_trace(mask: np.ndarray) -> List[Tuple[int, int]]:
|
307
|
+
"""
|
308
|
+
Trace the contour of a binary mask using an optimized approach.
|
309
|
+
Uses caching and simplified algorithms for better performance.
|
310
|
+
|
311
|
+
Args:
|
312
|
+
mask: Binary mask of the room (1 for room, 0 for background)
|
313
|
+
|
314
|
+
Returns:
|
315
|
+
List of (x, y) tuples representing the contour
|
316
|
+
"""
|
317
|
+
# Create a hash of the mask for caching
|
318
|
+
mask_hash = hash(mask.tobytes())
|
319
|
+
|
320
|
+
# Check if we have a cached result
|
321
|
+
if mask_hash in HypferRoomsHandler._hull_cache:
|
322
|
+
return HypferRoomsHandler._hull_cache[mask_hash]
|
323
|
+
|
324
|
+
# Fast path for empty masks
|
325
|
+
if not np.any(mask):
|
326
|
+
return []
|
327
|
+
|
328
|
+
# Find bounding box of non-zero elements (much faster than full labeling for simple cases)
|
329
|
+
y_indices, x_indices = np.where(mask > 0)
|
330
|
+
if len(x_indices) == 0 or len(y_indices) == 0:
|
331
|
+
return []
|
332
|
+
|
333
|
+
# For very small rooms (less than 100 pixels), just use bounding box
|
334
|
+
if len(x_indices) < 100:
|
335
|
+
x_min, x_max = np.min(x_indices), np.max(x_indices)
|
336
|
+
y_min, y_max = np.min(y_indices), np.max(y_indices)
|
337
|
+
|
338
|
+
# Create a simple rectangle
|
339
|
+
hull_vertices = [
|
340
|
+
(int(x_min), int(y_min)), # Top-left
|
341
|
+
(int(x_max), int(y_min)), # Top-right
|
342
|
+
(int(x_max), int(y_max)), # Bottom-right
|
343
|
+
(int(x_min), int(y_max)), # Bottom-left
|
344
|
+
(int(x_min), int(y_min)), # Back to top-left to close the polygon
|
345
|
+
]
|
346
|
+
|
347
|
+
# Cache and return the result
|
348
|
+
HypferRoomsHandler._hull_cache[mask_hash] = hull_vertices
|
349
|
+
return hull_vertices
|
350
|
+
|
351
|
+
# For larger rooms, use convex hull but with optimizations
|
352
|
+
try:
|
353
|
+
# Import here to avoid overhead for small rooms
|
354
|
+
from scipy import ndimage
|
355
|
+
from scipy.spatial import ConvexHull
|
356
|
+
|
357
|
+
# Use cached labeled array if available
|
358
|
+
if mask_hash in HypferRoomsHandler._label_cache:
|
359
|
+
labeled_array = HypferRoomsHandler._label_cache[mask_hash]
|
360
|
+
else:
|
361
|
+
# Find connected components - this is expensive
|
362
|
+
labeled_array, _ = ndimage.label(mask)
|
363
|
+
# Cache the result for future use
|
364
|
+
HypferRoomsHandler._label_cache[mask_hash] = labeled_array
|
365
|
+
|
366
|
+
# Limit cache size to prevent memory issues
|
367
|
+
if len(HypferRoomsHandler._label_cache) > 50: # Keep only 50 most recent items
|
368
|
+
# Remove oldest item (first key)
|
369
|
+
try:
|
370
|
+
HypferRoomsHandler._label_cache.pop(next(iter(HypferRoomsHandler._label_cache)))
|
371
|
+
except (StopIteration, KeyError):
|
372
|
+
# Handle edge case of empty cache
|
373
|
+
pass
|
374
|
+
|
375
|
+
# Create a mask with all components
|
376
|
+
all_components_mask = (labeled_array > 0)
|
377
|
+
|
378
|
+
# Sample points instead of using all points for large masks
|
379
|
+
# This significantly reduces computation time for ConvexHull
|
380
|
+
if len(x_indices) > 1000:
|
381
|
+
# Sample every 10th point for very large rooms
|
382
|
+
step = 10
|
383
|
+
elif len(x_indices) > 500:
|
384
|
+
# Sample every 5th point for medium-sized rooms
|
385
|
+
step = 5
|
386
|
+
else:
|
387
|
+
# Use all points for smaller rooms
|
388
|
+
step = 1
|
389
|
+
|
390
|
+
# Sample points using the step size
|
391
|
+
sampled_y = y_indices[::step]
|
392
|
+
sampled_x = x_indices[::step]
|
393
|
+
|
394
|
+
# Create a list of points
|
395
|
+
points = np.column_stack((sampled_x, sampled_y))
|
396
|
+
|
397
|
+
# Compute the convex hull
|
398
|
+
hull = ConvexHull(points)
|
399
|
+
|
400
|
+
# Extract the vertices of the convex hull
|
401
|
+
hull_vertices = [(int(points[v, 0]), int(points[v, 1])) for v in hull.vertices]
|
402
|
+
|
403
|
+
# Ensure the hull is closed
|
404
|
+
if hull_vertices[0] != hull_vertices[-1]:
|
405
|
+
hull_vertices.append(hull_vertices[0])
|
406
|
+
|
407
|
+
# Cache and return the result
|
408
|
+
HypferRoomsHandler._hull_cache[mask_hash] = hull_vertices
|
409
|
+
|
410
|
+
# Limit hull cache size
|
411
|
+
if len(HypferRoomsHandler._hull_cache) > 50:
|
412
|
+
try:
|
413
|
+
HypferRoomsHandler._hull_cache.pop(next(iter(HypferRoomsHandler._hull_cache)))
|
414
|
+
except (StopIteration, KeyError):
|
415
|
+
pass
|
416
|
+
|
417
|
+
return hull_vertices
|
418
|
+
|
419
|
+
except Exception as e:
|
420
|
+
LOGGER.warning(f"Failed to compute convex hull: {e}. Falling back to bounding box.")
|
421
|
+
|
422
|
+
# Fallback to bounding box if convex hull fails
|
423
|
+
x_min, x_max = np.min(x_indices), np.max(x_indices)
|
424
|
+
y_min, y_max = np.min(y_indices), np.max(y_indices)
|
425
|
+
|
426
|
+
# Create a simple rectangle
|
427
|
+
hull_vertices = [
|
428
|
+
(int(x_min), int(y_min)), # Top-left
|
429
|
+
(int(x_max), int(y_min)), # Top-right
|
430
|
+
(int(x_max), int(y_max)), # Bottom-right
|
431
|
+
(int(x_min), int(y_max)), # Bottom-left
|
432
|
+
(int(x_min), int(y_min)), # Back to top-left to close the polygon
|
433
|
+
]
|
434
|
+
|
435
|
+
# Cache and return the result
|
436
|
+
HypferRoomsHandler._hull_cache[mask_hash] = hull_vertices
|
437
|
+
return hull_vertices
|
438
|
+
|
439
|
+
|
440
|
+
|
441
|
+
async def async_extract_room_properties(
|
442
|
+
self, json_data: Dict[str, Any]
|
443
|
+
) -> RoomsProperties:
|
444
|
+
"""
|
445
|
+
Extract room properties from the JSON data.
|
446
|
+
|
447
|
+
Args:
|
448
|
+
json_data: JSON data from the vacuum
|
449
|
+
|
450
|
+
Returns:
|
451
|
+
Dictionary of room properties
|
452
|
+
"""
|
453
|
+
room_properties = {}
|
454
|
+
pixel_size = json_data.get("pixelSize", 5)
|
455
|
+
height = json_data["size"]["y"]
|
456
|
+
width = json_data["size"]["x"]
|
457
|
+
vacuum_id = self.vacuum_id
|
458
|
+
room_id_counter = 0
|
459
|
+
|
460
|
+
# Store the JSON data for reference in other methods
|
461
|
+
self.current_json_data = json_data
|
462
|
+
|
463
|
+
for layer in json_data.get("layers", []):
|
464
|
+
if layer.get("__class") == "MapLayer" and layer.get("type") == "segment":
|
465
|
+
meta_data = layer.get("metaData", {})
|
466
|
+
segment_id = meta_data.get("segmentId")
|
467
|
+
name = meta_data.get("name", f"Room {segment_id}")
|
468
|
+
|
469
|
+
# Check if this room is disabled in the drawing configuration
|
470
|
+
# The room_id_counter is 0-based, but DrawableElement.ROOM_X is 1-based
|
471
|
+
current_room_id = room_id_counter + 1
|
472
|
+
room_id_counter = (
|
473
|
+
room_id_counter + 1
|
474
|
+
) % 16 # Cycle room_id back to 0 after 15
|
475
|
+
|
476
|
+
if 1 <= current_room_id <= 15 and self.drawing_config is not None:
|
477
|
+
room_element = getattr(
|
478
|
+
DrawableElement, f"ROOM_{current_room_id}", None
|
479
|
+
)
|
480
|
+
if room_element and not self.drawing_config.is_enabled(
|
481
|
+
room_element
|
482
|
+
):
|
483
|
+
LOGGER.debug(
|
484
|
+
"%s: Room %d is disabled and will be skipped",
|
485
|
+
self.vacuum_id,
|
486
|
+
current_room_id,
|
487
|
+
)
|
488
|
+
continue
|
489
|
+
|
490
|
+
compressed_pixels = layer.get("compressedPixels", [])
|
491
|
+
pixels = self.sublist(compressed_pixels, 3)
|
492
|
+
|
493
|
+
# Create a binary mask for the room
|
494
|
+
if not pixels:
|
495
|
+
LOGGER.warning(f"Skipping segment {segment_id}: no pixels found")
|
496
|
+
continue
|
497
|
+
|
498
|
+
mask = np.zeros((height, width), dtype=np.uint8)
|
499
|
+
for x, y, length in pixels:
|
500
|
+
if 0 <= y < height and 0 <= x < width and x + length <= width:
|
501
|
+
mask[y, x : x + length] = 1
|
502
|
+
|
503
|
+
# Find the room outline using the improved get_corners function
|
504
|
+
# Adjust epsilon_factor to control the level of simplification (higher = fewer points)
|
505
|
+
outline = await self.async_get_corners(mask, epsilon_factor=0.05)
|
506
|
+
|
507
|
+
if not outline:
|
508
|
+
LOGGER.warning(
|
509
|
+
f"Skipping segment {segment_id}: failed to generate outline"
|
510
|
+
)
|
511
|
+
continue
|
512
|
+
|
513
|
+
# Calculate the center of the room
|
514
|
+
xs, ys = zip(*outline)
|
515
|
+
x_min, x_max = min(xs), max(xs)
|
516
|
+
y_min, y_max = min(ys), max(ys)
|
517
|
+
|
518
|
+
# Scale coordinates by pixel_size
|
519
|
+
scaled_outline = [(x * pixel_size, y * pixel_size) for x, y in outline]
|
520
|
+
|
521
|
+
room_id = str(segment_id)
|
522
|
+
room_properties[room_id] = {
|
523
|
+
"number": segment_id,
|
524
|
+
"outline": scaled_outline, # Already includes the closing point
|
525
|
+
"name": name,
|
526
|
+
"x": ((x_min + x_max) * pixel_size) // 2,
|
527
|
+
"y": ((y_min + y_max) * pixel_size) // 2,
|
528
|
+
}
|
529
|
+
|
530
|
+
RoomStore(vacuum_id, room_properties)
|
531
|
+
return room_properties
|
532
|
+
|
533
|
+
async def get_room_at_position(
|
534
|
+
self, x: int, y: int, room_properties: Optional[RoomsProperties] = None
|
535
|
+
) -> Optional[Dict[str, Any]]:
|
536
|
+
"""
|
537
|
+
Get the room at a specific position.
|
538
|
+
|
539
|
+
Args:
|
540
|
+
x: X coordinate
|
541
|
+
y: Y coordinate
|
542
|
+
room_properties: Room properties dictionary (optional)
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
Room data dictionary or None if no room at position
|
546
|
+
"""
|
547
|
+
if room_properties is None:
|
548
|
+
room_store = RoomStore(self.vacuum_id)
|
549
|
+
room_properties = room_store.get_rooms()
|
550
|
+
|
551
|
+
if not room_properties:
|
552
|
+
return None
|
553
|
+
|
554
|
+
for room_id, room_data in room_properties.items():
|
555
|
+
outline = room_data.get("outline", [])
|
556
|
+
if not outline or len(outline) < 3:
|
557
|
+
continue
|
558
|
+
|
559
|
+
# Check if point is inside the polygon
|
560
|
+
if self.point_in_polygon(x, y, outline):
|
561
|
+
return {
|
562
|
+
"id": room_id,
|
563
|
+
"name": room_data.get("name", f"Room {room_id}"),
|
564
|
+
"x": room_data.get("x", 0),
|
565
|
+
"y": room_data.get("y", 0),
|
566
|
+
}
|
567
|
+
|
568
|
+
return None
|
569
|
+
|
570
|
+
@staticmethod
|
571
|
+
def point_in_polygon(x: int, y: int, polygon: List[Tuple[int, int]]) -> bool:
|
572
|
+
"""
|
573
|
+
Check if a point is inside a polygon using ray casting algorithm.
|
574
|
+
|
575
|
+
Args:
|
576
|
+
x: X coordinate of the point
|
577
|
+
y: Y coordinate of the point
|
578
|
+
polygon: List of (x, y) tuples forming the polygon
|
579
|
+
|
580
|
+
Returns:
|
581
|
+
True if the point is inside the polygon, False otherwise
|
582
|
+
"""
|
583
|
+
n = len(polygon)
|
584
|
+
inside = False
|
585
|
+
|
586
|
+
p1x, p1y = polygon[0]
|
587
|
+
xinters = None # Initialize with default value
|
588
|
+
for i in range(1, n + 1):
|
589
|
+
p2x, p2y = polygon[i % n]
|
590
|
+
if y > min(p1y, p2y):
|
591
|
+
if y <= max(p1y, p2y):
|
592
|
+
if x <= max(p1x, p2x):
|
593
|
+
if p1y != p2y:
|
594
|
+
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
595
|
+
if p1x == p2x or x <= xinters:
|
596
|
+
inside = not inside
|
597
|
+
p1x, p1y = p2x, p2y
|
598
|
+
|
599
|
+
return inside
|
@@ -1,406 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Hipfer Rooms Handler Module.
|
3
|
-
Handles room data extraction and processing for Valetudo Hipfer vacuum maps.
|
4
|
-
Provides async methods for room outline extraction and properties management.
|
5
|
-
Version: 0.1.9
|
6
|
-
"""
|
7
|
-
|
8
|
-
from __future__ import annotations
|
9
|
-
|
10
|
-
from math import sqrt
|
11
|
-
from typing import Any, Dict, Optional, List, Tuple
|
12
|
-
|
13
|
-
import numpy as np
|
14
|
-
|
15
|
-
from .config.drawable_elements import DrawableElement, DrawingConfig
|
16
|
-
from .config.types import LOGGER, RoomsProperties, RoomStore
|
17
|
-
|
18
|
-
|
19
|
-
class HypferRoomsHandler:
|
20
|
-
"""
|
21
|
-
Handler for extracting and managing room data from Hipfer vacuum maps.
|
22
|
-
|
23
|
-
This class provides methods to:
|
24
|
-
- Extract room outlines using the Ramer-Douglas-Peucker algorithm
|
25
|
-
- Process room properties from JSON data
|
26
|
-
- Generate room masks and extract contours
|
27
|
-
|
28
|
-
All methods are async for better integration with the rest of the codebase.
|
29
|
-
"""
|
30
|
-
|
31
|
-
def __init__(self, vacuum_id: str, drawing_config: Optional[DrawingConfig] = None):
|
32
|
-
"""
|
33
|
-
Initialize the HipferRoomsHandler.
|
34
|
-
|
35
|
-
Args:
|
36
|
-
vacuum_id: Identifier for the vacuum
|
37
|
-
drawing_config: Configuration for which elements to draw (optional)
|
38
|
-
"""
|
39
|
-
self.vacuum_id = vacuum_id
|
40
|
-
self.drawing_config = drawing_config
|
41
|
-
|
42
|
-
@staticmethod
|
43
|
-
def sublist(data: list, chunk_size: int) -> list:
|
44
|
-
return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
|
45
|
-
|
46
|
-
@staticmethod
|
47
|
-
def perpendicular_distance(
|
48
|
-
point: tuple[int, int], line_start: tuple[int, int], line_end: tuple[int, int]
|
49
|
-
) -> float:
|
50
|
-
"""Calculate the perpendicular distance from a point to a line."""
|
51
|
-
if line_start == line_end:
|
52
|
-
return sqrt(
|
53
|
-
(point[0] - line_start[0]) ** 2 + (point[1] - line_start[1]) ** 2
|
54
|
-
)
|
55
|
-
|
56
|
-
x, y = point
|
57
|
-
x1, y1 = line_start
|
58
|
-
x2, y2 = line_end
|
59
|
-
|
60
|
-
# Calculate the line length
|
61
|
-
line_length = sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
62
|
-
if line_length == 0:
|
63
|
-
return 0
|
64
|
-
|
65
|
-
# Calculate the distance from the point to the line
|
66
|
-
return abs((y2 - y1) * x - (x2 - x1) * y + x2 * y1 - y2 * x1) / line_length
|
67
|
-
|
68
|
-
async def rdp(
|
69
|
-
self, points: List[Tuple[int, int]], epsilon: float
|
70
|
-
) -> List[Tuple[int, int]]:
|
71
|
-
"""Ramer-Douglas-Peucker algorithm for simplifying a curve."""
|
72
|
-
if len(points) <= 2:
|
73
|
-
return points
|
74
|
-
|
75
|
-
# Find the point with the maximum distance
|
76
|
-
dmax = 0
|
77
|
-
index = 0
|
78
|
-
for i in range(1, len(points) - 1):
|
79
|
-
d = self.perpendicular_distance(points[i], points[0], points[-1])
|
80
|
-
if d > dmax:
|
81
|
-
index = i
|
82
|
-
dmax = d
|
83
|
-
|
84
|
-
# If max distance is greater than epsilon, recursively simplify
|
85
|
-
if dmax > epsilon:
|
86
|
-
# Recursive call
|
87
|
-
first_segment = await self.rdp(points[: index + 1], epsilon)
|
88
|
-
second_segment = await self.rdp(points[index:], epsilon)
|
89
|
-
|
90
|
-
# Build the result list (avoiding duplicating the common point)
|
91
|
-
return first_segment[:-1] + second_segment
|
92
|
-
else:
|
93
|
-
return [points[0], points[-1]]
|
94
|
-
|
95
|
-
async def async_get_corners(
|
96
|
-
self, mask: np.ndarray, epsilon_factor: float = 0.05
|
97
|
-
) -> List[Tuple[int, int]]:
|
98
|
-
"""
|
99
|
-
Get the corners of a room shape as a list of (x, y) tuples.
|
100
|
-
Uses contour detection and Douglas-Peucker algorithm to simplify the contour.
|
101
|
-
|
102
|
-
Args:
|
103
|
-
mask: Binary mask of the room (1 for room, 0 for background)
|
104
|
-
epsilon_factor: Controls the level of simplification (higher = fewer points)
|
105
|
-
|
106
|
-
Returns:
|
107
|
-
List of (x, y) tuples representing the corners of the room
|
108
|
-
"""
|
109
|
-
# Find contours in the mask
|
110
|
-
contour = await self.async_moore_neighbor_trace(mask)
|
111
|
-
|
112
|
-
if not contour:
|
113
|
-
# Fallback to bounding box if contour detection fails
|
114
|
-
y_indices, x_indices = np.where(mask > 0)
|
115
|
-
if len(x_indices) == 0 or len(y_indices) == 0:
|
116
|
-
return []
|
117
|
-
|
118
|
-
x_min, x_max = np.min(x_indices), np.max(x_indices)
|
119
|
-
y_min, y_max = np.min(y_indices), np.max(y_indices)
|
120
|
-
|
121
|
-
return [
|
122
|
-
(x_min, y_min), # Top-left
|
123
|
-
(x_max, y_min), # Top-right
|
124
|
-
(x_max, y_max), # Bottom-right
|
125
|
-
(x_min, y_max), # Bottom-left
|
126
|
-
(x_min, y_min), # Back to top-left to close the polygon
|
127
|
-
]
|
128
|
-
|
129
|
-
# Calculate the perimeter of the contour
|
130
|
-
perimeter = 0
|
131
|
-
for i in range(len(contour) - 1):
|
132
|
-
x1, y1 = contour[i]
|
133
|
-
x2, y2 = contour[i + 1]
|
134
|
-
perimeter += np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
135
|
-
|
136
|
-
# Apply Douglas-Peucker algorithm to simplify the contour
|
137
|
-
epsilon = epsilon_factor * perimeter
|
138
|
-
simplified_contour = await self.rdp(contour, epsilon=epsilon)
|
139
|
-
|
140
|
-
# Ensure the contour has at least 3 points to form a polygon
|
141
|
-
if len(simplified_contour) < 3:
|
142
|
-
# Fallback to bounding box
|
143
|
-
y_indices, x_indices = np.where(mask > 0)
|
144
|
-
x_min, x_max = int(np.min(x_indices)), int(np.max(x_indices))
|
145
|
-
y_min, y_max = int(np.min(y_indices)), int(np.max(y_indices))
|
146
|
-
|
147
|
-
LOGGER.debug(
|
148
|
-
f"{self.vacuum_id}: Too few points in contour, using bounding box"
|
149
|
-
)
|
150
|
-
return [
|
151
|
-
(x_min, y_min), # Top-left
|
152
|
-
(x_max, y_min), # Top-right
|
153
|
-
(x_max, y_max), # Bottom-right
|
154
|
-
(x_min, y_max), # Bottom-left
|
155
|
-
(x_min, y_min), # Back to top-left to close the polygon
|
156
|
-
]
|
157
|
-
|
158
|
-
# Ensure the contour is closed
|
159
|
-
if simplified_contour[0] != simplified_contour[-1]:
|
160
|
-
simplified_contour.append(simplified_contour[0])
|
161
|
-
|
162
|
-
return simplified_contour
|
163
|
-
|
164
|
-
@staticmethod
|
165
|
-
async def async_moore_neighbor_trace(mask: np.ndarray) -> List[Tuple[int, int]]:
|
166
|
-
"""
|
167
|
-
Trace the contour of a binary mask using an optimized Moore-Neighbor tracing.
|
168
|
-
|
169
|
-
Args:
|
170
|
-
mask: Binary mask of the room (1 for room, 0 for background)
|
171
|
-
|
172
|
-
Returns:
|
173
|
-
List of (x, y) tuples representing the contour
|
174
|
-
"""
|
175
|
-
# Convert to uint8 and pad
|
176
|
-
padded = np.pad(mask.astype(np.uint8), 1, mode="constant")
|
177
|
-
height, width = padded.shape
|
178
|
-
|
179
|
-
# Find the first non-zero point efficiently (scan row by row)
|
180
|
-
# This is much faster than np.argwhere() for large arrays
|
181
|
-
start = None
|
182
|
-
for y in range(height):
|
183
|
-
# Use NumPy's any() to quickly check if there are any 1s in this row
|
184
|
-
if np.any(padded[y]):
|
185
|
-
# Find the first 1 in this row
|
186
|
-
x = np.where(padded[y] == 1)[0][0]
|
187
|
-
start = (int(x), int(y))
|
188
|
-
break
|
189
|
-
|
190
|
-
if start is None:
|
191
|
-
return []
|
192
|
-
|
193
|
-
# Pre-compute directions
|
194
|
-
directions = [
|
195
|
-
(-1, -1),
|
196
|
-
(-1, 0),
|
197
|
-
(-1, 1),
|
198
|
-
(0, 1),
|
199
|
-
(1, 1),
|
200
|
-
(1, 0),
|
201
|
-
(1, -1),
|
202
|
-
(0, -1),
|
203
|
-
]
|
204
|
-
|
205
|
-
# Use a 2D array for visited tracking (faster than set)
|
206
|
-
visited = np.zeros((height, width), dtype=bool)
|
207
|
-
|
208
|
-
# Initialize contour
|
209
|
-
contour = []
|
210
|
-
contour.append((int(start[0] - 1), int(start[1] - 1))) # Adjust for padding
|
211
|
-
|
212
|
-
current = start
|
213
|
-
prev_dir = 7
|
214
|
-
visited[current[1], current[0]] = True
|
215
|
-
|
216
|
-
# Main tracing loop
|
217
|
-
while True:
|
218
|
-
found = False
|
219
|
-
|
220
|
-
# Check all 8 directions
|
221
|
-
for i in range(8):
|
222
|
-
dir_idx = (prev_dir + i) % 8
|
223
|
-
dx, dy = directions[dir_idx]
|
224
|
-
nx, ny = current[0] + dx, current[1] + dy
|
225
|
-
|
226
|
-
# Bounds check and value check
|
227
|
-
if (
|
228
|
-
0 <= ny < height
|
229
|
-
and 0 <= nx < width
|
230
|
-
and padded[ny, nx] == 1
|
231
|
-
and not visited[ny, nx]
|
232
|
-
):
|
233
|
-
current = (nx, ny)
|
234
|
-
visited[ny, nx] = True
|
235
|
-
contour.append(
|
236
|
-
(int(nx - 1), int(ny - 1))
|
237
|
-
) # Adjust for padding and convert to int
|
238
|
-
prev_dir = (dir_idx + 5) % 8
|
239
|
-
found = True
|
240
|
-
break
|
241
|
-
|
242
|
-
# Check termination conditions
|
243
|
-
if not found or (
|
244
|
-
len(contour) > 3
|
245
|
-
and (int(current[0] - 1), int(current[1] - 1)) == contour[0]
|
246
|
-
):
|
247
|
-
break
|
248
|
-
|
249
|
-
return contour
|
250
|
-
|
251
|
-
async def async_extract_room_properties(
|
252
|
-
self, json_data: Dict[str, Any]
|
253
|
-
) -> RoomsProperties:
|
254
|
-
"""
|
255
|
-
Extract room properties from the JSON data.
|
256
|
-
|
257
|
-
Args:
|
258
|
-
json_data: JSON data from the vacuum
|
259
|
-
|
260
|
-
Returns:
|
261
|
-
Dictionary of room properties
|
262
|
-
"""
|
263
|
-
room_properties = {}
|
264
|
-
pixel_size = json_data.get("pixelSize", 5)
|
265
|
-
height = json_data["size"]["y"]
|
266
|
-
width = json_data["size"]["x"]
|
267
|
-
vacuum_id = self.vacuum_id
|
268
|
-
room_id_counter = 0
|
269
|
-
|
270
|
-
for layer in json_data.get("layers", []):
|
271
|
-
if layer.get("__class") == "MapLayer" and layer.get("type") == "segment":
|
272
|
-
meta_data = layer.get("metaData", {})
|
273
|
-
segment_id = meta_data.get("segmentId")
|
274
|
-
name = meta_data.get("name", f"Room {segment_id}")
|
275
|
-
|
276
|
-
# Check if this room is disabled in the drawing configuration
|
277
|
-
# The room_id_counter is 0-based, but DrawableElement.ROOM_X is 1-based
|
278
|
-
current_room_id = room_id_counter + 1
|
279
|
-
room_id_counter = (
|
280
|
-
room_id_counter + 1
|
281
|
-
) % 16 # Cycle room_id back to 0 after 15
|
282
|
-
|
283
|
-
if 1 <= current_room_id <= 15 and self.drawing_config is not None:
|
284
|
-
room_element = getattr(
|
285
|
-
DrawableElement, f"ROOM_{current_room_id}", None
|
286
|
-
)
|
287
|
-
if room_element and not self.drawing_config.is_enabled(
|
288
|
-
room_element
|
289
|
-
):
|
290
|
-
LOGGER.debug(
|
291
|
-
"%s: Room %d is disabled and will be skipped",
|
292
|
-
self.vacuum_id,
|
293
|
-
current_room_id,
|
294
|
-
)
|
295
|
-
continue
|
296
|
-
|
297
|
-
compressed_pixels = layer.get("compressedPixels", [])
|
298
|
-
pixels = self.sublist(compressed_pixels, 3)
|
299
|
-
|
300
|
-
# Create a binary mask for the room
|
301
|
-
if not pixels:
|
302
|
-
LOGGER.warning(f"Skipping segment {segment_id}: no pixels found")
|
303
|
-
continue
|
304
|
-
|
305
|
-
mask = np.zeros((height, width), dtype=np.uint8)
|
306
|
-
for x, y, length in pixels:
|
307
|
-
if 0 <= y < height and 0 <= x < width and x + length <= width:
|
308
|
-
mask[y, x : x + length] = 1
|
309
|
-
|
310
|
-
# Find the room outline using the improved get_corners function
|
311
|
-
# Adjust epsilon_factor to control the level of simplification (higher = fewer points)
|
312
|
-
outline = await self.async_get_corners(mask, epsilon_factor=0.05)
|
313
|
-
|
314
|
-
if not outline:
|
315
|
-
LOGGER.warning(
|
316
|
-
f"Skipping segment {segment_id}: failed to generate outline"
|
317
|
-
)
|
318
|
-
continue
|
319
|
-
|
320
|
-
# Calculate the center of the room
|
321
|
-
xs, ys = zip(*outline)
|
322
|
-
x_min, x_max = min(xs), max(xs)
|
323
|
-
y_min, y_max = min(ys), max(ys)
|
324
|
-
|
325
|
-
# Scale coordinates by pixel_size
|
326
|
-
scaled_outline = [(x * pixel_size, y * pixel_size) for x, y in outline]
|
327
|
-
|
328
|
-
room_id = str(segment_id)
|
329
|
-
room_properties[room_id] = {
|
330
|
-
"number": segment_id,
|
331
|
-
"outline": scaled_outline, # Already includes the closing point
|
332
|
-
"name": name,
|
333
|
-
"x": ((x_min + x_max) * pixel_size) // 2,
|
334
|
-
"y": ((y_min + y_max) * pixel_size) // 2,
|
335
|
-
}
|
336
|
-
|
337
|
-
RoomStore(vacuum_id, room_properties)
|
338
|
-
return room_properties
|
339
|
-
|
340
|
-
async def get_room_at_position(
|
341
|
-
self, x: int, y: int, room_properties: Optional[RoomsProperties] = None
|
342
|
-
) -> Optional[Dict[str, Any]]:
|
343
|
-
"""
|
344
|
-
Get the room at a specific position.
|
345
|
-
|
346
|
-
Args:
|
347
|
-
x: X coordinate
|
348
|
-
y: Y coordinate
|
349
|
-
room_properties: Room properties dictionary (optional)
|
350
|
-
|
351
|
-
Returns:
|
352
|
-
Room data dictionary or None if no room at position
|
353
|
-
"""
|
354
|
-
if room_properties is None:
|
355
|
-
room_store = RoomStore(self.vacuum_id)
|
356
|
-
room_properties = room_store.get_rooms()
|
357
|
-
|
358
|
-
if not room_properties:
|
359
|
-
return None
|
360
|
-
|
361
|
-
for room_id, room_data in room_properties.items():
|
362
|
-
outline = room_data.get("outline", [])
|
363
|
-
if not outline or len(outline) < 3:
|
364
|
-
continue
|
365
|
-
|
366
|
-
# Check if point is inside the polygon
|
367
|
-
if self.point_in_polygon(x, y, outline):
|
368
|
-
return {
|
369
|
-
"id": room_id,
|
370
|
-
"name": room_data.get("name", f"Room {room_id}"),
|
371
|
-
"x": room_data.get("x", 0),
|
372
|
-
"y": room_data.get("y", 0),
|
373
|
-
}
|
374
|
-
|
375
|
-
return None
|
376
|
-
|
377
|
-
@staticmethod
|
378
|
-
def point_in_polygon(x: int, y: int, polygon: List[Tuple[int, int]]) -> bool:
|
379
|
-
"""
|
380
|
-
Check if a point is inside a polygon using ray casting algorithm.
|
381
|
-
|
382
|
-
Args:
|
383
|
-
x: X coordinate of the point
|
384
|
-
y: Y coordinate of the point
|
385
|
-
polygon: List of (x, y) tuples forming the polygon
|
386
|
-
|
387
|
-
Returns:
|
388
|
-
True if the point is inside the polygon, False otherwise
|
389
|
-
"""
|
390
|
-
n = len(polygon)
|
391
|
-
inside = False
|
392
|
-
|
393
|
-
p1x, p1y = polygon[0]
|
394
|
-
xinters = None # Initialize with default value
|
395
|
-
for i in range(1, n + 1):
|
396
|
-
p2x, p2y = polygon[i % n]
|
397
|
-
if y > min(p1y, p2y):
|
398
|
-
if y <= max(p1y, p2y):
|
399
|
-
if x <= max(p1x, p2x):
|
400
|
-
if p1y != p2y:
|
401
|
-
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
402
|
-
if p1x == p2x or x <= xinters:
|
403
|
-
inside = not inside
|
404
|
-
p1x, p1y = p2x, p2y
|
405
|
-
|
406
|
-
return inside
|
File without changes
|
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/hypfer_draw.py
RENAMED
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/map_data.py
RENAMED
File without changes
|
{valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/py.typed
RENAMED
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b51 → valetudo_map_parser-0.1.9b53}/SCR/valetudo_map_parser/reimg_draw.py
RENAMED
File without changes
|