valetudo-map-parser 0.1.9b42__py3-none-any.whl → 0.1.9b44__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.
@@ -66,10 +66,10 @@ class EnhancedDrawable(Drawable):
66
66
 
67
67
  # Convert back to 0-255 range and return as tuple
68
68
  return (
69
- int(max(0, min(255, r_out))),
70
- int(max(0, min(255, g_out))),
71
- int(max(0, min(255, b_out))),
72
- int(max(0, min(255, a_out * 255))),
69
+ int(max(0, min(255, int(r_out)))),
70
+ int(max(0, min(255, int(g_out)))),
71
+ int(max(0, min(255, int(b_out)))),
72
+ int(max(0, min(255, int(a_out * 255)))),
73
73
  )
74
74
 
75
75
  def blend_pixel(
@@ -280,11 +280,6 @@ class EnhancedDrawable(Drawable):
280
280
  DrawableElement.ROBOT,
281
281
  element_map,
282
282
  )
283
-
284
- # TODO: Draw robot orientation indicator
285
- # This would be a line or triangle showing the direction
286
- # For now, we'll skip this part as it requires more complex drawing
287
-
288
283
  return array, element_map
289
284
 
290
285
  async def _draw_charger(
@@ -0,0 +1,363 @@
1
+ """
2
+ Optimized Element Map Generator.
3
+ Uses scipy for efficient element map generation and processing.
4
+ Version: 0.1.9
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import numpy as np
11
+ from scipy import ndimage
12
+
13
+ from .drawable_elements import DrawableElement, DrawingConfig
14
+ from .types import LOGGER
15
+
16
+
17
+ class OptimizedElementMapGenerator:
18
+ """Class for generating 2D element maps from JSON data with optimized performance.
19
+
20
+ This class creates a 2D array where each cell contains an integer code
21
+ representing the element at that position (floor, wall, room, etc.).
22
+ It uses scipy for efficient processing and supports sparse matrices for memory efficiency.
23
+ """
24
+
25
+ def __init__(self, drawing_config: DrawingConfig = None, shared_data=None):
26
+ """Initialize the optimized element map generator.
27
+
28
+ Args:
29
+ drawing_config: Optional drawing configuration for element properties
30
+ shared_data: Shared data object for accessing common resources
31
+ """
32
+ self.drawing_config = drawing_config or DrawingConfig()
33
+ self.shared = shared_data
34
+ self.element_map = None
35
+ self.element_map_shape = None
36
+ self.scale_info = None
37
+ self.file_name = getattr(shared_data, 'file_name', 'ElementMap') if shared_data else 'ElementMap'
38
+
39
+ async def async_generate_from_json(self, json_data, existing_element_map=None):
40
+ """Generate a 2D element map from JSON data with optimized performance.
41
+
42
+ Args:
43
+ json_data: The JSON data from the vacuum
44
+ existing_element_map: Optional pre-created element map to populate
45
+
46
+ Returns:
47
+ numpy.ndarray: The 2D element map
48
+ """
49
+ if not self.shared:
50
+ LOGGER.warning("Shared data not provided, some features may not work.")
51
+ return None
52
+
53
+ # Use existing element map if provided
54
+ if existing_element_map is not None:
55
+ self.element_map = existing_element_map
56
+ return existing_element_map
57
+
58
+ # Detect JSON format
59
+ is_valetudo = "layers" in json_data and "pixelSize" in json_data
60
+ is_rand256 = "map_data" in json_data
61
+
62
+ if not (is_valetudo or is_rand256):
63
+ LOGGER.error("Unknown JSON format, cannot generate element map")
64
+ return None
65
+
66
+ if is_valetudo:
67
+ return await self._generate_valetudo_element_map(json_data)
68
+ elif is_rand256:
69
+ return await self._generate_rand256_element_map(json_data)
70
+
71
+ async def _generate_valetudo_element_map(self, json_data):
72
+ """Generate an element map from Valetudo format JSON data."""
73
+ # Get map dimensions from the JSON data
74
+ size_x = json_data["size"]["x"]
75
+ size_y = json_data["size"]["y"]
76
+ pixel_size = json_data["pixelSize"]
77
+
78
+ # Calculate downscale factor based on pixel size
79
+ # Standard pixel size is 5mm, so adjust accordingly
80
+ downscale_factor = max(1, pixel_size // 5 * 2) # More aggressive downscaling
81
+
82
+ # Calculate dimensions for the downscaled map
83
+ map_width = max(100, size_x // (pixel_size * downscale_factor))
84
+ map_height = max(100, size_y // (pixel_size * downscale_factor))
85
+
86
+ LOGGER.info(
87
+ "%s: Creating optimized element map with dimensions: %dx%d (downscale factor: %d)",
88
+ self.file_name,
89
+ map_width, map_height, downscale_factor
90
+ )
91
+
92
+ # Create the element map at the reduced size
93
+ element_map = np.zeros((map_height, map_width), dtype=np.int32)
94
+ element_map[:] = DrawableElement.FLOOR
95
+
96
+ # Store scaling information for coordinate conversion
97
+ self.scale_info = {
98
+ "original_size": (size_x, size_y),
99
+ "map_size": (map_width, map_height),
100
+ "scale_factor": downscale_factor * pixel_size,
101
+ "pixel_size": pixel_size
102
+ }
103
+
104
+ # Process layers at the reduced resolution
105
+ for layer in json_data.get("layers", []):
106
+ layer_type = layer.get("type")
107
+
108
+ # Process rooms (segments)
109
+ if layer_type == "segment":
110
+ # Get room ID
111
+ meta_data = layer.get("metaData", {})
112
+ segment_id = meta_data.get("segmentId")
113
+
114
+ if segment_id is not None:
115
+ # Convert segment_id to int if it's a string
116
+ segment_id_int = int(segment_id) if isinstance(segment_id, str) else segment_id
117
+ if 1 <= segment_id_int <= 15:
118
+ room_element = getattr(DrawableElement, f"ROOM_{segment_id_int}", None)
119
+
120
+ # Skip if room is disabled
121
+ if room_element is None or not self.drawing_config.is_enabled(room_element):
122
+ continue
123
+
124
+ # Create a temporary high-resolution mask for this room
125
+ temp_mask = np.zeros((size_y // pixel_size, size_x // pixel_size), dtype=np.uint8)
126
+
127
+ # Process pixels for this room
128
+ compressed_pixels = layer.get("compressedPixels", [])
129
+ if compressed_pixels:
130
+ # Process in chunks of 3 (x, y, count)
131
+ for i in range(0, len(compressed_pixels), 3):
132
+ if i + 2 < len(compressed_pixels):
133
+ x = compressed_pixels[i]
134
+ y = compressed_pixels[i+1]
135
+ count = compressed_pixels[i+2]
136
+
137
+ # Set pixels in the high-resolution mask
138
+ for j in range(count):
139
+ px = x + j
140
+ if 0 <= y < temp_mask.shape[0] and 0 <= px < temp_mask.shape[1]:
141
+ temp_mask[y, px] = 1
142
+
143
+ # Use scipy to downsample the mask efficiently
144
+ # This preserves the room shape better than simple decimation
145
+ downsampled_mask = ndimage.zoom(
146
+ temp_mask,
147
+ (map_height / temp_mask.shape[0], map_width / temp_mask.shape[1]),
148
+ order=0 # Nearest neighbor interpolation
149
+ )
150
+
151
+ # Apply the downsampled mask to the element map
152
+ element_map[downsampled_mask > 0] = room_element
153
+
154
+ # Clean up
155
+ del temp_mask, downsampled_mask
156
+
157
+ # Process walls similarly
158
+ elif layer_type == "wall" and self.drawing_config.is_enabled(DrawableElement.WALL):
159
+ # Create a temporary high-resolution mask for walls
160
+ temp_mask = np.zeros((size_y // pixel_size, size_x // pixel_size), dtype=np.uint8)
161
+
162
+ # Process compressed pixels for walls
163
+ compressed_pixels = layer.get("compressedPixels", [])
164
+ if compressed_pixels:
165
+ # Process in chunks of 3 (x, y, count)
166
+ for i in range(0, len(compressed_pixels), 3):
167
+ if i + 2 < len(compressed_pixels):
168
+ x = compressed_pixels[i]
169
+ y = compressed_pixels[i+1]
170
+ count = compressed_pixels[i+2]
171
+
172
+ # Set pixels in the high-resolution mask
173
+ for j in range(count):
174
+ px = x + j
175
+ if 0 <= y < temp_mask.shape[0] and 0 <= px < temp_mask.shape[1]:
176
+ temp_mask[y, px] = 1
177
+
178
+ # Use scipy to downsample the mask efficiently
179
+ downsampled_mask = ndimage.zoom(
180
+ temp_mask,
181
+ (map_height / temp_mask.shape[0], map_width / temp_mask.shape[1]),
182
+ order=0
183
+ )
184
+
185
+ # Apply the downsampled mask to the element map
186
+ # Only overwrite floor pixels, not room pixels
187
+ wall_mask = (downsampled_mask > 0) & (element_map == DrawableElement.FLOOR)
188
+ element_map[wall_mask] = DrawableElement.WALL
189
+
190
+ # Clean up
191
+ del temp_mask, downsampled_mask
192
+
193
+ # Store the element map
194
+ self.element_map = element_map
195
+ self.element_map_shape = element_map.shape
196
+
197
+ LOGGER.info(
198
+ "%s: Element map generation complete with shape: %s",
199
+ self.file_name,
200
+ element_map.shape
201
+ )
202
+ return element_map
203
+
204
+ async def _generate_rand256_element_map(self, json_data):
205
+ """Generate an element map from Rand256 format JSON data."""
206
+ # Get map dimensions from the Rand256 JSON data
207
+ map_data = json_data["map_data"]
208
+ size_x = map_data["dimensions"]["width"]
209
+ size_y = map_data["dimensions"]["height"]
210
+
211
+ # Calculate downscale factor
212
+ downscale_factor = max(1, min(size_x, size_y) // 500) # Target ~500px in smallest dimension
213
+
214
+ # Calculate dimensions for the downscaled map
215
+ map_width = max(100, size_x // downscale_factor)
216
+ map_height = max(100, size_y // downscale_factor)
217
+
218
+ LOGGER.info(
219
+ "%s: Creating optimized Rand256 element map with dimensions: %dx%d (downscale factor: %d)",
220
+ self.file_name,
221
+ map_width, map_height, downscale_factor
222
+ )
223
+
224
+ # Create the element map at the reduced size
225
+ element_map = np.zeros((map_height, map_width), dtype=np.int32)
226
+ element_map[:] = DrawableElement.FLOOR
227
+
228
+ # Store scaling information for coordinate conversion
229
+ self.scale_info = {
230
+ "original_size": (size_x, size_y),
231
+ "map_size": (map_width, map_height),
232
+ "scale_factor": downscale_factor,
233
+ "pixel_size": 1 # Rand256 uses 1:1 pixel mapping
234
+ }
235
+
236
+ # Process rooms
237
+ if "rooms" in map_data and map_data["rooms"]:
238
+ for room in map_data["rooms"]:
239
+ # Get room ID and check if it's enabled
240
+ room_id_int = room["id"]
241
+
242
+ # Get room element code (ROOM_1, ROOM_2, etc.)
243
+ room_element = None
244
+ if 0 < room_id_int <= 15:
245
+ room_element = getattr(DrawableElement, f"ROOM_{room_id_int}", None)
246
+
247
+ # Skip if room is disabled
248
+ if room_element is None or not self.drawing_config.is_enabled(room_element):
249
+ continue
250
+
251
+ if "coordinates" in room:
252
+ # Create a high-resolution mask for this room
253
+ temp_mask = np.zeros((size_y, size_x), dtype=np.uint8)
254
+
255
+ # Fill the mask with room coordinates
256
+ for coord in room["coordinates"]:
257
+ x, y = coord
258
+ if 0 <= y < size_y and 0 <= x < size_x:
259
+ temp_mask[y, x] = 1
260
+
261
+ # Use scipy to downsample the mask efficiently
262
+ downsampled_mask = ndimage.zoom(
263
+ temp_mask,
264
+ (map_height / size_y, map_width / size_x),
265
+ order=0 # Nearest neighbor interpolation
266
+ )
267
+
268
+ # Apply the downsampled mask to the element map
269
+ element_map[downsampled_mask > 0] = room_element
270
+
271
+ # Clean up
272
+ del temp_mask, downsampled_mask
273
+
274
+ # Process walls
275
+ if "walls" in map_data and map_data["walls"] and self.drawing_config.is_enabled(DrawableElement.WALL):
276
+ # Create a high-resolution mask for walls
277
+ temp_mask = np.zeros((size_y, size_x), dtype=np.uint8)
278
+
279
+ # Fill the mask with wall coordinates
280
+ for coord in map_data["walls"]:
281
+ x, y = coord
282
+ if 0 <= y < size_y and 0 <= x < size_x:
283
+ temp_mask[y, x] = 1
284
+
285
+ # Use scipy to downsample the mask efficiently
286
+ downsampled_mask = ndimage.zoom(
287
+ temp_mask,
288
+ (map_height / size_y, map_width / size_x),
289
+ order=0
290
+ )
291
+
292
+ # Apply the downsampled mask to the element map
293
+ # Only overwrite floor pixels, not room pixels
294
+ wall_mask = (downsampled_mask > 0) & (element_map == DrawableElement.FLOOR)
295
+ element_map[wall_mask] = DrawableElement.WALL
296
+
297
+ # Clean up
298
+ del temp_mask, downsampled_mask
299
+
300
+ # Store the element map
301
+ self.element_map = element_map
302
+ self.element_map_shape = element_map.shape
303
+
304
+ LOGGER.info(
305
+ "%s: Rand256 element map generation complete with shape: %s",
306
+ self.file_name,
307
+ element_map.shape
308
+ )
309
+ return element_map
310
+
311
+ def map_to_element_coordinates(self, x, y):
312
+ """Convert map coordinates to element map coordinates."""
313
+ if not hasattr(self, 'scale_info'):
314
+ return x, y
315
+
316
+ scale = self.scale_info["scale_factor"]
317
+ return int(x / scale), int(y / scale)
318
+
319
+ def element_to_map_coordinates(self, x, y):
320
+ """Convert element map coordinates to map coordinates."""
321
+ if not hasattr(self, 'scale_info'):
322
+ return x, y
323
+
324
+ scale = self.scale_info["scale_factor"]
325
+ return int(x * scale), int(y * scale)
326
+
327
+ def get_element_at_position(self, x, y):
328
+ """Get the element at the specified position."""
329
+ if not hasattr(self, 'element_map') or self.element_map is None:
330
+ return None
331
+
332
+ if not (0 <= y < self.element_map.shape[0] and 0 <= x < self.element_map.shape[1]):
333
+ return None
334
+
335
+ return self.element_map[y, x]
336
+
337
+ def get_room_at_position(self, x, y):
338
+ """Get the room ID at a specific position, or None if not a room."""
339
+ element_code = self.get_element_at_position(x, y)
340
+ if element_code is None:
341
+ return None
342
+
343
+ # Check if it's a room (codes 101-115)
344
+ if 101 <= element_code <= 115:
345
+ return element_code
346
+ return None
347
+
348
+ def get_element_name(self, element_code):
349
+ """Get the name of the element from its code."""
350
+ if element_code is None:
351
+ return 'NONE'
352
+
353
+ # Check if it's a room
354
+ if element_code >= 100:
355
+ room_number = element_code - 100
356
+ return f'ROOM_{room_number}'
357
+
358
+ # Check standard elements
359
+ for name, code in vars(DrawableElement).items():
360
+ if not name.startswith('_') and isinstance(code, int) and code == element_code:
361
+ return name
362
+
363
+ return f'UNKNOWN_{element_code}'
@@ -0,0 +1,148 @@
1
+ """
2
+ Room Outline Extraction Utilities.
3
+ Uses scipy for efficient room outline extraction.
4
+ Version: 0.1.9
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ from scipy import ndimage
11
+
12
+ from .types import LOGGER
13
+
14
+
15
+ async def extract_room_outline_with_scipy(
16
+ room_mask, min_x, min_y, max_x, max_y, file_name=None, room_id=None
17
+ ):
18
+ """Extract a room outline using scipy for contour finding.
19
+
20
+ Args:
21
+ room_mask: Binary mask of the room (1 for room, 0 for non-room)
22
+ min_x, min_y, max_x, max_y: Bounding box coordinates
23
+ file_name: Optional file name for logging
24
+ room_id: Optional room ID for logging
25
+
26
+ Returns:
27
+ List of points forming the outline of the room
28
+ """
29
+ # If the mask is empty, return a rectangular outline
30
+ if np.sum(room_mask) == 0:
31
+ LOGGER.warning(
32
+ "%s: Empty room mask for room %s, using rectangular outline",
33
+ file_name or "RoomOutline",
34
+ str(room_id) if room_id is not None else "unknown",
35
+ )
36
+ return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
37
+
38
+ # Use scipy to clean up the mask (remove noise, fill holes)
39
+ # Fill small holes
40
+ room_mask = ndimage.binary_fill_holes(room_mask).astype(np.uint8)
41
+
42
+ # Remove small objects
43
+ labeled_array, num_features = ndimage.label(room_mask)
44
+ if num_features > 1:
45
+ # Find the largest connected component
46
+ component_sizes = np.bincount(labeled_array.ravel())[1:]
47
+ largest_component = np.argmax(component_sizes) + 1
48
+ room_mask = (labeled_array == largest_component).astype(np.uint8)
49
+
50
+ # Find the boundary points by tracing the perimeter
51
+ boundary_points = []
52
+ height, width = room_mask.shape
53
+
54
+ # Scan horizontally (top and bottom edges)
55
+ for x in range(width):
56
+ # Top edge
57
+ for y in range(height):
58
+ if room_mask[y, x] == 1:
59
+ boundary_points.append((x + min_x, y + min_y))
60
+ break
61
+
62
+ # Bottom edge
63
+ for y in range(height-1, -1, -1):
64
+ if room_mask[y, x] == 1:
65
+ boundary_points.append((x + min_x, y + min_y))
66
+ break
67
+
68
+ # Scan vertically (left and right edges)
69
+ for y in range(height):
70
+ # Left edge
71
+ for x in range(width):
72
+ if room_mask[y, x] == 1:
73
+ boundary_points.append((x + min_x, y + min_y))
74
+ break
75
+
76
+ # Right edge
77
+ for x in range(width-1, -1, -1):
78
+ if room_mask[y, x] == 1:
79
+ boundary_points.append((x + min_x, y + min_y))
80
+ break
81
+
82
+ # Remove duplicates while preserving order
83
+ unique_points = []
84
+ for point in boundary_points:
85
+ if point not in unique_points:
86
+ unique_points.append(point)
87
+
88
+ # If we have too few points, return a simple rectangle
89
+ if len(unique_points) < 4:
90
+ LOGGER.warning(
91
+ "%s: Too few boundary points for room %s, using rectangular outline",
92
+ file_name or "RoomOutline",
93
+ str(room_id) if room_id is not None else "unknown",
94
+ )
95
+ return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
96
+
97
+ # Simplify the outline by keeping only significant points
98
+ simplified = simplify_outline(unique_points, tolerance=5)
99
+
100
+ LOGGER.debug(
101
+ "%s: Extracted outline for room %s with %d points",
102
+ file_name or "RoomOutline",
103
+ str(room_id) if room_id is not None else "unknown",
104
+ len(simplified),
105
+ )
106
+
107
+ return simplified
108
+
109
+
110
+ def simplify_outline(points, tolerance=5):
111
+ """Simplify an outline by removing points that don't contribute much to the shape."""
112
+ if len(points) <= 4:
113
+ return points
114
+
115
+ # Start with the first point
116
+ simplified = [points[0]]
117
+
118
+ # Process remaining points
119
+ for i in range(1, len(points) - 1):
120
+ # Get previous and next points
121
+ prev = simplified[-1]
122
+ current = points[i]
123
+ next_point = points[i + 1]
124
+
125
+ # Calculate vectors
126
+ v1 = (current[0] - prev[0], current[1] - prev[1])
127
+ v2 = (next_point[0] - current[0], next_point[1] - current[1])
128
+
129
+ # Calculate change in direction
130
+ dot_product = v1[0] * v2[0] + v1[1] * v2[1]
131
+ len_v1 = (v1[0]**2 + v1[1]**2)**0.5
132
+ len_v2 = (v2[0]**2 + v2[1]**2)**0.5
133
+
134
+ # Avoid division by zero
135
+ if len_v1 == 0 or len_v2 == 0:
136
+ continue
137
+
138
+ # Calculate cosine of angle between vectors
139
+ cos_angle = dot_product / (len_v1 * len_v2)
140
+
141
+ # If angle is significant or distance is large, keep the point
142
+ if abs(cos_angle) < 0.95 or len_v1 > tolerance or len_v2 > tolerance:
143
+ simplified.append(current)
144
+
145
+ # Add the last point
146
+ simplified.append(points[-1])
147
+
148
+ return simplified
@@ -107,6 +107,7 @@ class CameraShared:
107
107
  self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"]) # Trims data
108
108
  self.skip_room_ids: List[str] = []
109
109
  self.device_info = None # Store the device_info
110
+ self.element_map = None # Map of element codes
110
111
 
111
112
  def update_user_colors(self, user_colors):
112
113
  """Update the user colors."""