valetudo-map-parser 0.1.10rc7__py3-none-any.whl → 0.1.11b1__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.
@@ -1,7 +1,8 @@
1
- """New Rand256 Map Parser - Based on Xiaomi/Roborock implementation with precise binary parsing."""
1
+ """New Rand256 Map Parser -
2
+ Based on Xiaomi/Roborock implementation with precise binary parsing."""
2
3
 
3
- import struct
4
4
  import math
5
+ import struct
5
6
  from enum import Enum
6
7
  from typing import Any, Dict, List, Optional
7
8
 
@@ -78,6 +79,7 @@ class RRMapParser:
78
79
 
79
80
  @staticmethod
80
81
  def _parse_carpet_map(data: bytes) -> set[int]:
82
+ """Parse carpet map using Xiaomi method."""
81
83
  carpet_map = set()
82
84
 
83
85
  for i, v in enumerate(data):
@@ -87,6 +89,7 @@ class RRMapParser:
87
89
 
88
90
  @staticmethod
89
91
  def _parse_area(header: bytes, data: bytes) -> list:
92
+ """Parse area using Xiaomi method."""
90
93
  area_pairs = RRMapParser._get_int16(header, 0x08)
91
94
  areas = []
92
95
  for area_start in range(0, area_pairs * 16, 16):
@@ -114,6 +117,7 @@ class RRMapParser:
114
117
 
115
118
  @staticmethod
116
119
  def _parse_zones(data: bytes, header: bytes) -> list:
120
+ """Parse zones using Xiaomi method."""
117
121
  zone_pairs = RRMapParser._get_int16(header, 0x08)
118
122
  zones = []
119
123
  for zone_start in range(0, zone_pairs * 8, 8):
@@ -146,9 +150,9 @@ class RRMapParser:
146
150
  angle = raw_angle
147
151
  return {"position": [x, y], "angle": angle}
148
152
 
149
-
150
153
  @staticmethod
151
154
  def _parse_walls(data: bytes, header: bytes) -> list:
155
+ """Parse walls using Xiaomi method."""
152
156
  wall_pairs = RRMapParser._get_int16(header, 0x08)
153
157
  walls = []
154
158
  for wall_start in range(0, wall_pairs * 8, 8):
@@ -156,7 +160,14 @@ class RRMapParser:
156
160
  y0 = RRMapParser._get_int16(data, wall_start + 2)
157
161
  x1 = RRMapParser._get_int16(data, wall_start + 4)
158
162
  y1 = RRMapParser._get_int16(data, wall_start + 6)
159
- walls.append([x0, RRMapParser.Tools.DIMENSION_MM - y0, x1, RRMapParser.Tools.DIMENSION_MM - y1])
163
+ walls.append(
164
+ [
165
+ x0,
166
+ RRMapParser.Tools.DIMENSION_MM - y0,
167
+ x1,
168
+ RRMapParser.Tools.DIMENSION_MM - y1,
169
+ ]
170
+ )
160
171
  return walls
161
172
 
162
173
  @staticmethod
@@ -204,6 +215,7 @@ class RRMapParser:
204
215
  return {}
205
216
 
206
217
  def parse_blocks(self, raw: bytes, pixels: bool = True) -> Dict[int, Any]:
218
+ """Parse all blocks using Xiaomi method."""
207
219
  blocks = {}
208
220
  map_header_length = self._get_int16(raw, 0x02)
209
221
  block_start_position = map_header_length
@@ -218,29 +230,53 @@ class RRMapParser:
218
230
  match block_type:
219
231
  case self.Types.DIGEST.value:
220
232
  self.is_valid = True
221
- case self.Types.ROBOT_POSITION.value | self.Types.CHARGER_LOCATION.value:
222
- blocks[block_type] = self._parse_object_position(block_data_length, data)
233
+ case (
234
+ self.Types.ROBOT_POSITION.value
235
+ | self.Types.CHARGER_LOCATION.value
236
+ ):
237
+ blocks[block_type] = self._parse_object_position(
238
+ block_data_length, data
239
+ )
223
240
  case self.Types.PATH.value | self.Types.GOTO_PREDICTED_PATH.value:
224
- blocks[block_type] = self._parse_path_block(raw, block_start_position, block_data_length)
241
+ blocks[block_type] = self._parse_path_block(
242
+ raw, block_start_position, block_data_length
243
+ )
225
244
  case self.Types.CURRENTLY_CLEANED_ZONES.value:
226
245
  blocks[block_type] = {"zones": self._parse_zones(data, header)}
227
246
  case self.Types.FORBIDDEN_ZONES.value:
228
- blocks[block_type] = {"forbidden_zones": self._parse_area(header, data)}
247
+ blocks[block_type] = {
248
+ "forbidden_zones": self._parse_area(header, data)
249
+ }
229
250
  case self.Types.FORBIDDEN_MOP_ZONES.value:
230
- blocks[block_type] = {"forbidden_mop_zones": self._parse_area(header, data)}
251
+ blocks[block_type] = {
252
+ "forbidden_mop_zones": self._parse_area(header, data)
253
+ }
231
254
  case self.Types.GOTO_TARGET.value:
232
255
  blocks[block_type] = {"position": self._parse_goto_target(data)}
233
256
  case self.Types.VIRTUAL_WALLS.value:
234
- blocks[block_type] = {"virtual_walls": self._parse_walls(data, header)}
257
+ blocks[block_type] = {
258
+ "virtual_walls": self._parse_walls(data, header)
259
+ }
235
260
  case self.Types.CARPET_MAP.value:
236
- data = RRMapParser._get_bytes(raw, block_data_start, block_data_length)
237
- blocks[block_type] = {"carpet_map": self._parse_carpet_map(data)}
261
+ data = RRMapParser._get_bytes(
262
+ raw, block_data_start, block_data_length
263
+ )
264
+ blocks[block_type] = {
265
+ "carpet_map": self._parse_carpet_map(data)
266
+ }
238
267
  case self.Types.IMAGE.value:
239
268
  header_length = self._get_int8(header, 2)
240
269
  blocks[block_type] = self._parse_image_block(
241
- raw, block_start_position, block_data_length, header_length, pixels)
242
-
243
- block_start_position = block_start_position + block_data_length + self._get_int8(header, 2)
270
+ raw,
271
+ block_start_position,
272
+ block_data_length,
273
+ header_length,
274
+ pixels,
275
+ )
276
+
277
+ block_start_position = (
278
+ block_start_position + block_data_length + self._get_int8(header, 2)
279
+ )
244
280
  except (struct.error, IndexError):
245
281
  break
246
282
  return blocks
@@ -7,18 +7,20 @@ Version: v0.1.12
7
7
  import asyncio
8
8
  import logging
9
9
  from typing import List
10
+
10
11
  from PIL import Image
11
12
 
13
+ from .utils import pil_size_rotation
12
14
  from .types import (
13
15
  ATTR_CALIBRATION_POINTS,
14
16
  ATTR_CAMERA_MODE,
15
17
  ATTR_CONTENT_TYPE,
18
+ ATTR_IMAGE_LAST_UPDATED,
16
19
  ATTR_MARGINS,
17
20
  ATTR_OBSTACLES,
18
21
  ATTR_POINTS,
19
22
  ATTR_ROOMS,
20
23
  ATTR_ROTATE,
21
- ATTR_IMAGE_LAST_UPDATED,
22
24
  ATTR_VACUUM_BATTERY,
23
25
  ATTR_VACUUM_CHARGING,
24
26
  ATTR_VACUUM_JSON_ID,
@@ -40,8 +42,8 @@ from .types import (
40
42
  DEFAULT_VALUES,
41
43
  CameraModes,
42
44
  Colors,
43
- TrimsData,
44
45
  PilPNG,
46
+ TrimsData,
45
47
  )
46
48
 
47
49
 
@@ -209,11 +211,12 @@ class CameraShared:
209
211
 
210
212
  def to_dict(self) -> dict:
211
213
  """Return a dictionary with image and attributes data."""
214
+
212
215
  return {
213
216
  "image": {
214
217
  "binary": self.binary_image,
215
218
  "pil_image": self.new_image,
216
- "size": self.new_image.size if self.new_image else (10, 10),
219
+ "size": pil_size_rotation(self.image_rotate, self.new_image),
217
220
  },
218
221
  "attributes": self.generate_attributes(),
219
222
  }
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  from ..types import LOGGER, PilPNG
10
10
  from .translations import translations
11
11
 
12
+
12
13
  LOGGER.propagate = True
13
14
 
14
15
 
@@ -8,7 +8,7 @@ import json
8
8
  import logging
9
9
  import threading
10
10
  from dataclasses import asdict, dataclass
11
- from typing import Any, Dict, Optional, Tuple, TypedDict, Union, List, NotRequired
11
+ from typing import Any, Dict, List, NotRequired, Optional, Tuple, TypedDict, Union
12
12
 
13
13
  import numpy as np
14
14
  from PIL import Image
@@ -34,12 +34,11 @@ class Room(TypedDict):
34
34
  id: int
35
35
 
36
36
 
37
- # list[dict[str, str | list[int]]] | list[dict[str, str | list[list[int]]]] | list[dict[str, str | int]] | int]'
38
37
  class Destinations(TypedDict, total=False):
39
38
  spots: NotRequired[Optional[List[Spot]]]
40
39
  zones: NotRequired[Optional[List[Zone]]]
41
40
  rooms: NotRequired[Optional[List[Room]]]
42
- updated: NotRequired[Optional[float]]
41
+ updated: NotRequired[Optional[float | int]]
43
42
 
44
43
 
45
44
  class RoomProperty(TypedDict):
@@ -222,7 +221,9 @@ class SnapshotStore:
222
221
  Color = Union[Tuple[int, int, int], Tuple[int, int, int, int]]
223
222
  Colors = Dict[str, Color]
224
223
  CalibrationPoints = list[dict[str, Any]]
225
- RobotPosition = Optional[dict[str, Union[int | float]]]
224
+ RobotPosition: type[tuple[Any, Any, dict[str, int | float] | None]] = tuple[
225
+ Any, Any, dict[str, int | float] | None
226
+ ]
226
227
  ChargerPosition = dict[str, Any]
227
228
  RoomsProperties = dict[str, RoomProperty]
228
229
  ImageSize = dict[str, int | list[int]]
@@ -1,32 +1,30 @@
1
1
  """Utility code for the valetudo map parser."""
2
2
 
3
3
  import datetime
4
- from time import time
5
4
  import hashlib
5
+ import io
6
6
  import json
7
7
  from dataclasses import dataclass
8
+ from time import time
8
9
  from typing import Callable, List, Optional, Tuple
9
- import io
10
10
 
11
11
  import numpy as np
12
12
  from PIL import Image, ImageOps
13
13
 
14
+ from ..map_data import HyperMapData
15
+ from .async_utils import AsyncNumPy
14
16
  from .drawable import Drawable
15
17
  from .drawable_elements import DrawingConfig
16
- from .enhanced_drawable import EnhancedDrawable
17
18
  from .status_text.status_text import StatusText
18
-
19
19
  from .types import (
20
20
  LOGGER,
21
21
  ChargerPosition,
22
- Size,
22
+ Destinations,
23
23
  NumpyArray,
24
24
  PilPNG,
25
25
  RobotPosition,
26
- Destinations,
26
+ Size,
27
27
  )
28
- from ..map_data import HyperMapData
29
- from .async_utils import AsyncNumPy
30
28
 
31
29
 
32
30
  @dataclass
@@ -79,7 +77,6 @@ class BaseHandler:
79
77
  # Drawing components are initialized by initialize_drawing_config in handlers
80
78
  self.drawing_config: Optional[DrawingConfig] = None
81
79
  self.draw: Optional[Drawable] = None
82
- self.enhanced_draw: Optional[EnhancedDrawable] = None
83
80
 
84
81
  def get_frame_number(self) -> int:
85
82
  """Return the frame number of the image."""
@@ -114,6 +111,12 @@ class BaseHandler:
114
111
  try:
115
112
  # Backup current image to last_image before processing new one
116
113
  if hasattr(self.shared, "new_image") and self.shared.new_image is not None:
114
+ # Close old last_image to free memory before replacing it
115
+ if hasattr(self.shared, "last_image") and self.shared.last_image is not None:
116
+ try:
117
+ self.shared.last_image.close()
118
+ except Exception:
119
+ pass # Ignore errors if image is already closed
117
120
  self.shared.last_image = self.shared.new_image
118
121
 
119
122
  # Call the appropriate handler method based on handler type
@@ -199,10 +202,11 @@ class BaseHandler:
199
202
  if hasattr(self, "get_rooms_attributes") and (
200
203
  self.shared.map_rooms is None and destinations is not None
201
204
  ):
202
- (self.shared.map_rooms,) = await self.get_rooms_attributes(destinations)
205
+ self.shared.map_rooms = await self.get_rooms_attributes(destinations)
203
206
  if self.shared.map_rooms:
204
207
  LOGGER.debug("%s: Rand256 attributes rooms updated", self.file_name)
205
208
 
209
+
206
210
  if hasattr(self, "async_get_rooms_attributes") and (
207
211
  self.shared.map_rooms is None
208
212
  ):
@@ -233,15 +237,8 @@ class BaseHandler:
233
237
  self, pil_img: PilPNG, rand: bool = False
234
238
  ) -> ResizeParams:
235
239
  """Prepare resize parameters for image resizing."""
236
- if self.shared.image_rotate in [0, 180]:
237
- width, height = pil_img.size
238
- else:
239
- height, width = pil_img.size
240
- LOGGER.debug(
241
- "Shared PIL image size: %s x %s",
242
- self.shared.image_ref_width,
243
- self.shared.image_ref_height,
244
- )
240
+ width, height = pil_size_rotation(self.shared.image_rotate, pil_img)
241
+
245
242
  return ResizeParams(
246
243
  pil_img=pil_img,
247
244
  width=width,
@@ -662,9 +659,6 @@ class BaseHandler:
662
659
 
663
660
  async def async_resize_image(params: ResizeParams):
664
661
  """Resize the image to the given dimensions and aspect ratio."""
665
- LOGGER.debug("Resizing image to aspect ratio: %s", params.aspect_ratio)
666
- LOGGER.debug("Original image size: %s x %s", params.width, params.height)
667
- LOGGER.debug("Image crop size: %s", params.crop_size)
668
662
  if params.aspect_ratio == "None":
669
663
  return params.pil_img
670
664
  if params.aspect_ratio != "None":
@@ -701,6 +695,17 @@ async def async_resize_image(params: ResizeParams):
701
695
  return params.pil_img
702
696
 
703
697
 
698
+ def pil_size_rotation(image_rotate, pil_img):
699
+ """Return the size of the image."""
700
+ if not pil_img:
701
+ return 0, 0
702
+ if image_rotate in [0, 180]:
703
+ width, height = pil_img.size
704
+ else:
705
+ height, width = pil_img.size
706
+ return width, height
707
+
708
+
704
709
  def initialize_drawing_config(handler):
705
710
  """
706
711
  Initialize drawing configuration from device_info.
@@ -709,7 +714,7 @@ def initialize_drawing_config(handler):
709
714
  handler: The handler instance with shared data and file_name attributes
710
715
 
711
716
  Returns:
712
- Tuple of (DrawingConfig, Drawable, EnhancedDrawable)
717
+ Tuple of (DrawingConfig, Drawable)
713
718
  """
714
719
 
715
720
  # Initialize drawing configuration
@@ -721,98 +726,10 @@ def initialize_drawing_config(handler):
721
726
  ):
722
727
  drawing_config.update_from_device_info(handler.shared.device_info)
723
728
 
724
- # Initialize both drawable systems for backward compatibility
725
- draw = Drawable() # Legacy drawing utilities
726
- enhanced_draw = EnhancedDrawable(drawing_config) # New enhanced drawing system
729
+ # Initialize drawing utilities
730
+ draw = Drawable()
727
731
 
728
- return drawing_config, draw, enhanced_draw
729
-
730
-
731
- def blend_colors(base_color, overlay_color):
732
- """
733
- Blend two RGBA colors using alpha compositing.
734
-
735
- Args:
736
- base_color: Base RGBA color tuple (r, g, b, a)
737
- overlay_color: Overlay RGBA color tuple (r, g, b, a)
738
-
739
- Returns:
740
- Blended RGBA color tuple (r, g, b, a)
741
- """
742
- r1, g1, b1, a1 = base_color
743
- r2, g2, b2, a2 = overlay_color
744
-
745
- # Convert alpha to 0-1 range
746
- a1 = a1 / 255.0
747
- a2 = a2 / 255.0
748
-
749
- # Calculate resulting alpha
750
- a_out = a1 + a2 * (1 - a1)
751
-
752
- # Avoid division by zero
753
- if a_out < 0.0001:
754
- return [0, 0, 0, 0]
755
-
756
- # Calculate blended RGB components
757
- r_out = (r1 * a1 + r2 * a2 * (1 - a1)) / a_out
758
- g_out = (g1 * a1 + g2 * a2 * (1 - a1)) / a_out
759
- b_out = (b1 * a1 + b2 * a2 * (1 - a1)) / a_out
760
-
761
- # Convert back to 0-255 range and return as tuple
762
- return (
763
- int(max(0, min(255, r_out))),
764
- int(max(0, min(255, g_out))),
765
- int(max(0, min(255, b_out))),
766
- int(max(0, min(255, a_out * 255))),
767
- )
768
-
769
-
770
- def blend_pixel(array, x, y, color, element, element_map=None, drawing_config=None):
771
- """
772
- Blend a pixel color with the existing color at the specified position.
773
- Also updates the element map if the new element has higher z-index.
774
-
775
- Args:
776
- array: The image array to modify
777
- x: X coordinate
778
- y: Y coordinate
779
- color: RGBA color tuple to blend
780
- element: Element code for the pixel
781
- element_map: Optional element map to update
782
- drawing_config: Optional drawing configuration for z-index lookup
783
-
784
- Returns:
785
- None
786
- """
787
- # Check bounds
788
- if not (0 <= y < array.shape[0] and 0 <= x < array.shape[1]):
789
- return
790
-
791
- # Get current element at this position
792
- current_element = None
793
- if element_map is not None:
794
- current_element = element_map[y, x]
795
-
796
- # Get z-index values for comparison
797
- current_z = 0
798
- new_z = 0
799
-
800
- if drawing_config is not None:
801
- current_z = (
802
- drawing_config.get_property(current_element, "z_index", 0)
803
- if current_element
804
- else 0
805
- )
806
- new_z = drawing_config.get_property(element, "z_index", 0)
807
-
808
- # Update element map if new element has higher z-index
809
- if element_map is not None and new_z >= current_z:
810
- element_map[y, x] = element
811
-
812
- # Blend colors
813
- base_color = array[y, x]
814
- blended_color = blend_colors(base_color, color)
815
- array[y, x] = blended_color
732
+ return drawing_config, draw
816
733
 
817
734
 
818
735
  def manage_drawable_elements(
@@ -996,12 +913,6 @@ async def async_extract_room_outline(
996
913
 
997
914
  # If we found too few boundary points, use the rectangular outline
998
915
  if len(boundary_points) < 8: # Need at least 8 points for a meaningful shape
999
- LOGGER.debug(
1000
- "%s: Room %s has too few boundary points (%d), using rectangular outline",
1001
- file_name,
1002
- str(room_id_int),
1003
- len(boundary_points),
1004
- )
1005
916
  return rect_outline
1006
917
 
1007
918
  # Use a more sophisticated algorithm to create a coherent outline
@@ -1037,13 +948,6 @@ async def async_extract_room_outline(
1037
948
  # Convert NumPy int64 values to regular Python integers
1038
949
  simplified_outline = [(int(x), int(y)) for x, y in simplified_outline]
1039
950
 
1040
- LOGGER.debug(
1041
- "%s: Room %s outline has %d points",
1042
- file_name,
1043
- str(room_id_int),
1044
- len(simplified_outline),
1045
- )
1046
-
1047
951
  return simplified_outline
1048
952
 
1049
953
  except (ValueError, IndexError, TypeError, ArithmeticError) as e:
@@ -269,8 +269,6 @@ class ImageDraw:
269
269
  zone_clean = self.img_h.data.find_zone_entities(m_json)
270
270
  except (ValueError, KeyError):
271
271
  zone_clean = None
272
- else:
273
- _LOGGER.info("%s: Got zones.", self.file_name)
274
272
 
275
273
  if zone_clean:
276
274
  # Process zones sequentially to avoid memory-intensive array copies
@@ -8,24 +8,22 @@ Version: 0.1.10
8
8
  from __future__ import annotations
9
9
 
10
10
  import asyncio
11
- import numpy as np
12
11
 
12
+ import numpy as np
13
+ from mvcrender.autocrop import AutoCrop
13
14
  from PIL import Image
14
15
 
15
16
  from .config.async_utils import AsyncPIL
16
-
17
- from mvcrender.autocrop import AutoCrop
18
17
  from .config.drawable_elements import DrawableElement
19
18
  from .config.shared import CameraShared
20
-
21
19
  from .config.types import (
22
20
  COLORS,
23
21
  LOGGER,
24
22
  CalibrationPoints,
25
23
  Colors,
24
+ JsonType,
26
25
  RoomsProperties,
27
26
  RoomStore,
28
- JsonType,
29
27
  )
30
28
  from .config.utils import (
31
29
  BaseHandler,
@@ -48,9 +46,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
48
46
  self.calibration_data = None # camera shared data.
49
47
  self.data = ImageData # imported Image Data Module.
50
48
  # Initialize drawing configuration using the shared utility function
51
- self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(
52
- self
53
- )
49
+ self.drawing_config, self.draw = initialize_drawing_config(self)
54
50
 
55
51
  self.go_to = None # vacuum go to data
56
52
  self.img_hash = None # hash of the image calculated to check differences.
@@ -77,7 +73,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
77
73
  json_data
78
74
  )
79
75
  if room_properties:
80
- rooms = RoomStore(self.file_name, room_properties)
76
+ _ = RoomStore(self.file_name, room_properties)
81
77
  # Convert room_properties to the format expected by async_get_robot_in_room
82
78
  self.rooms_pos = []
83
79
  for room_id, room_data in room_properties.items():
@@ -258,7 +254,12 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
258
254
  )
259
255
  LOGGER.info("%s: Completed base Layers", self.file_name)
260
256
  # Copy the new array in base layer.
257
+ # Delete old base layer before creating new one to free memory
258
+ if self.img_base_layer is not None:
259
+ del self.img_base_layer
261
260
  self.img_base_layer = await self.async_copy_array(img_np_array)
261
+ # Delete source array after copying to free memory
262
+ del img_np_array
262
263
 
263
264
  self.shared.frame_number = self.frame_number
264
265
  self.frame_number += 1
@@ -272,6 +273,9 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
272
273
  or self.img_work_layer.shape != self.img_base_layer.shape
273
274
  or self.img_work_layer.dtype != self.img_base_layer.dtype
274
275
  ):
276
+ # Delete old buffer before creating new one to free memory
277
+ if self.img_work_layer is not None:
278
+ del self.img_work_layer
275
279
  self.img_work_layer = np.empty_like(self.img_base_layer)
276
280
 
277
281
  # Copy the base layer into the persistent working buffer (no new allocation per frame)
@@ -346,16 +350,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
346
350
  robot_state=self.shared.vacuum_state,
347
351
  )
348
352
 
349
- # Update element map for robot position
350
- if (
351
- hasattr(self.shared, "element_map")
352
- and self.shared.element_map is not None
353
- ):
354
- update_element_map_with_robot(
355
- self.shared.element_map,
356
- robot_position,
357
- DrawableElement.ROBOT,
358
- )
359
353
  # Synchronize zooming state from ImageDraw to handler before auto-crop
360
354
  self.zooming = self.imd.img_h.zooming
361
355
 
@@ -8,22 +8,22 @@ Version: v0.1.10
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import numpy as np
11
+ from dataclasses import asdict, dataclass, field
12
12
  from typing import (
13
- List,
14
- Sequence,
15
- TypeVar,
16
13
  Any,
17
- TypedDict,
18
- NotRequired,
19
14
  Literal,
15
+ NotRequired,
20
16
  Optional,
17
+ Sequence,
18
+ TypedDict,
19
+ TypeVar,
21
20
  )
22
21
 
23
- from dataclasses import dataclass, field, asdict
22
+ import numpy as np
24
23
 
25
24
  from .config.types import ImageSize, JsonType
26
25
 
26
+
27
27
  T = TypeVar("T")
28
28
 
29
29
  # --- Common Nested Structures ---
@@ -373,6 +373,11 @@ class ImageData:
373
373
  Else:
374
374
  (min_x_mm, min_y_mm, max_x_mm, max_y_mm)
375
375
  """
376
+
377
+ def to_mm(coord):
378
+ """Convert pixel coordinates to millimeters."""
379
+ return round(coord * pixel_size * 10)
380
+
376
381
  if not pixels:
377
382
  raise ValueError("Pixels list cannot be empty.")
378
383
 
@@ -393,7 +398,6 @@ class ImageData:
393
398
  min_y = min(min_y, y)
394
399
 
395
400
  if rand:
396
- to_mm = lambda v: v * pixel_size * 10
397
401
  return (to_mm(max_x), to_mm(max_y)), (to_mm(min_x), to_mm(min_y))
398
402
 
399
403
  return (
@@ -548,8 +552,9 @@ class RandImageData:
548
552
  @staticmethod
549
553
  def get_rrm_forbidden_zones(json_data: JsonType) -> list[dict[str, Any]]:
550
554
  """Get the forbidden zones from the json."""
551
- re_zones = json_data.get("forbidden_zones", [])
552
- re_zones.extend(json_data.get("forbidden_mop_zones", []))
555
+ re_zones = json_data.get("forbidden_zones", []) + json_data.get(
556
+ "forbidden_mop_zones", []
557
+ )
553
558
  formatted_zones = RandImageData._rrm_valetudo_format_zone(re_zones)
554
559
  return formatted_zones
555
560