valetudo-map-parser 0.1.9b41__py3-none-any.whl → 0.1.9b43__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 +6 -3
- valetudo_map_parser/config/drawable.py +22 -2
- valetudo_map_parser/config/drawable_elements.py +312 -0
- valetudo_map_parser/config/enhanced_drawable.py +447 -0
- valetudo_map_parser/config/shared.py +27 -0
- valetudo_map_parser/config/types.py +2 -1
- valetudo_map_parser/config/utils.py +410 -1
- valetudo_map_parser/hypfer_draw.py +194 -60
- valetudo_map_parser/hypfer_handler.py +344 -40
- valetudo_map_parser/map_data.py +1 -1
- valetudo_map_parser/rand25_handler.py +224 -39
- valetudo_map_parser/reimg_draw.py +1 -1
- valetudo_map_parser-0.1.9b43.dist-info/METADATA +92 -0
- valetudo_map_parser-0.1.9b43.dist-info/RECORD +23 -0
- {valetudo_map_parser-0.1.9b41.dist-info → valetudo_map_parser-0.1.9b43.dist-info}/WHEEL +1 -1
- valetudo_map_parser-0.1.9b41.dist-info/METADATA +0 -47
- valetudo_map_parser-0.1.9b41.dist-info/RECORD +0 -21
- {valetudo_map_parser-0.1.9b41.dist-info → valetudo_map_parser-0.1.9b43.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b41.dist-info → valetudo_map_parser-0.1.9b43.dist-info}/NOTICE.txt +0 -0
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
import hashlib
|
4
4
|
import json
|
5
|
+
import logging
|
6
|
+
import numpy as np
|
5
7
|
from dataclasses import dataclass
|
6
8
|
from logging import getLogger
|
7
|
-
from typing import Callable, List, Optional
|
9
|
+
from typing import Callable, Dict, List, Optional, Tuple, Union
|
8
10
|
|
9
11
|
from PIL import ImageOps
|
10
12
|
|
@@ -530,3 +532,410 @@ def prepare_resize_params(handler, pil_img, rand):
|
|
530
532
|
offset_func=handler.async_map_coordinates_offset,
|
531
533
|
is_rand=rand,
|
532
534
|
)
|
535
|
+
|
536
|
+
|
537
|
+
def initialize_drawing_config(handler):
|
538
|
+
"""
|
539
|
+
Initialize drawing configuration from device_info.
|
540
|
+
|
541
|
+
Args:
|
542
|
+
handler: The handler instance with shared data and file_name attributes
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
Tuple of (DrawingConfig, Drawable, EnhancedDrawable)
|
546
|
+
"""
|
547
|
+
from .drawable import Drawable
|
548
|
+
from .drawable_elements import DrawableElement, DrawingConfig
|
549
|
+
from .enhanced_drawable import EnhancedDrawable
|
550
|
+
|
551
|
+
# Initialize drawing configuration
|
552
|
+
drawing_config = DrawingConfig()
|
553
|
+
|
554
|
+
# Get logger from the handler
|
555
|
+
_LOGGER = logging.getLogger(handler.__class__.__module__)
|
556
|
+
|
557
|
+
if hasattr(handler.shared, "device_info") and handler.shared.device_info is not None:
|
558
|
+
_LOGGER.info(
|
559
|
+
"%s: Initializing drawing config from device_info", handler.file_name
|
560
|
+
)
|
561
|
+
_LOGGER.info(
|
562
|
+
"%s: device_info contains disable_obstacles: %s",
|
563
|
+
handler.file_name,
|
564
|
+
"disable_obstacles" in handler.shared.device_info,
|
565
|
+
)
|
566
|
+
_LOGGER.info(
|
567
|
+
"%s: device_info contains disable_path: %s",
|
568
|
+
handler.file_name,
|
569
|
+
"disable_path" in handler.shared.device_info,
|
570
|
+
)
|
571
|
+
_LOGGER.info(
|
572
|
+
"%s: device_info contains disable_elements: %s",
|
573
|
+
handler.file_name,
|
574
|
+
"disable_elements" in handler.shared.device_info,
|
575
|
+
)
|
576
|
+
|
577
|
+
if "disable_obstacles" in handler.shared.device_info:
|
578
|
+
_LOGGER.info(
|
579
|
+
"%s: disable_obstacles value: %s",
|
580
|
+
handler.file_name,
|
581
|
+
handler.shared.device_info["disable_obstacles"],
|
582
|
+
)
|
583
|
+
if "disable_path" in handler.shared.device_info:
|
584
|
+
_LOGGER.info(
|
585
|
+
"%s: disable_path value: %s",
|
586
|
+
handler.file_name,
|
587
|
+
handler.shared.device_info["disable_path"],
|
588
|
+
)
|
589
|
+
if "disable_elements" in handler.shared.device_info:
|
590
|
+
_LOGGER.info(
|
591
|
+
"%s: disable_elements value: %s",
|
592
|
+
handler.file_name,
|
593
|
+
handler.shared.device_info["disable_elements"],
|
594
|
+
)
|
595
|
+
|
596
|
+
drawing_config.update_from_device_info(handler.shared.device_info)
|
597
|
+
|
598
|
+
# Verify elements are disabled
|
599
|
+
_LOGGER.info(
|
600
|
+
"%s: After initialization, PATH enabled: %s",
|
601
|
+
handler.file_name,
|
602
|
+
drawing_config.is_enabled(DrawableElement.PATH),
|
603
|
+
)
|
604
|
+
_LOGGER.info(
|
605
|
+
"%s: After initialization, OBSTACLE enabled: %s",
|
606
|
+
handler.file_name,
|
607
|
+
drawing_config.is_enabled(DrawableElement.OBSTACLE),
|
608
|
+
)
|
609
|
+
|
610
|
+
# Initialize both drawable systems for backward compatibility
|
611
|
+
draw = Drawable() # Legacy drawing utilities
|
612
|
+
enhanced_draw = EnhancedDrawable(drawing_config) # New enhanced drawing system
|
613
|
+
|
614
|
+
return drawing_config, draw, enhanced_draw
|
615
|
+
|
616
|
+
|
617
|
+
def blend_colors(base_color, overlay_color):
|
618
|
+
"""
|
619
|
+
Blend two RGBA colors using alpha compositing.
|
620
|
+
|
621
|
+
Args:
|
622
|
+
base_color: Base RGBA color tuple (r, g, b, a)
|
623
|
+
overlay_color: Overlay RGBA color tuple (r, g, b, a)
|
624
|
+
|
625
|
+
Returns:
|
626
|
+
Blended RGBA color tuple (r, g, b, a)
|
627
|
+
"""
|
628
|
+
r1, g1, b1, a1 = base_color
|
629
|
+
r2, g2, b2, a2 = overlay_color
|
630
|
+
|
631
|
+
# Convert alpha to 0-1 range
|
632
|
+
a1 = a1 / 255.0
|
633
|
+
a2 = a2 / 255.0
|
634
|
+
|
635
|
+
# Calculate resulting alpha
|
636
|
+
a_out = a1 + a2 * (1 - a1)
|
637
|
+
|
638
|
+
# Avoid division by zero
|
639
|
+
if a_out < 0.0001:
|
640
|
+
return (0, 0, 0, 0)
|
641
|
+
|
642
|
+
# Calculate blended RGB components
|
643
|
+
r_out = (r1 * a1 + r2 * a2 * (1 - a1)) / a_out
|
644
|
+
g_out = (g1 * a1 + g2 * a2 * (1 - a1)) / a_out
|
645
|
+
b_out = (b1 * a1 + b2 * a2 * (1 - a1)) / a_out
|
646
|
+
|
647
|
+
# Convert back to 0-255 range and return as tuple
|
648
|
+
return (
|
649
|
+
int(max(0, min(255, r_out))),
|
650
|
+
int(max(0, min(255, g_out))),
|
651
|
+
int(max(0, min(255, b_out))),
|
652
|
+
int(max(0, min(255, a_out * 255))),
|
653
|
+
)
|
654
|
+
|
655
|
+
|
656
|
+
def blend_pixel(array, x, y, color, element, element_map=None, drawing_config=None):
|
657
|
+
"""
|
658
|
+
Blend a pixel color with the existing color at the specified position.
|
659
|
+
Also updates the element map if the new element has higher z-index.
|
660
|
+
|
661
|
+
Args:
|
662
|
+
array: The image array to modify
|
663
|
+
x: X coordinate
|
664
|
+
y: Y coordinate
|
665
|
+
color: RGBA color tuple to blend
|
666
|
+
element: Element code for the pixel
|
667
|
+
element_map: Optional element map to update
|
668
|
+
drawing_config: Optional drawing configuration for z-index lookup
|
669
|
+
|
670
|
+
Returns:
|
671
|
+
None
|
672
|
+
"""
|
673
|
+
# Check bounds
|
674
|
+
if not (0 <= y < array.shape[0] and 0 <= x < array.shape[1]):
|
675
|
+
return
|
676
|
+
|
677
|
+
# Get current element at this position
|
678
|
+
current_element = None
|
679
|
+
if element_map is not None:
|
680
|
+
current_element = element_map[y, x]
|
681
|
+
|
682
|
+
# Get z-index values for comparison
|
683
|
+
current_z = 0
|
684
|
+
new_z = 0
|
685
|
+
|
686
|
+
if drawing_config is not None:
|
687
|
+
current_z = (
|
688
|
+
drawing_config.get_property(current_element, "z_index", 0)
|
689
|
+
if current_element
|
690
|
+
else 0
|
691
|
+
)
|
692
|
+
new_z = drawing_config.get_property(element, "z_index", 0)
|
693
|
+
|
694
|
+
# Update element map if new element has higher z-index
|
695
|
+
if element_map is not None and new_z >= current_z:
|
696
|
+
element_map[y, x] = element
|
697
|
+
|
698
|
+
# Blend colors
|
699
|
+
base_color = array[y, x]
|
700
|
+
blended_color = blend_colors(base_color, color)
|
701
|
+
array[y, x] = blended_color
|
702
|
+
|
703
|
+
|
704
|
+
def get_element_at_position(element_map, x, y):
|
705
|
+
"""
|
706
|
+
Get the element code at a specific position in the element map.
|
707
|
+
|
708
|
+
Args:
|
709
|
+
element_map: The element map array
|
710
|
+
x: X coordinate
|
711
|
+
y: Y coordinate
|
712
|
+
|
713
|
+
Returns:
|
714
|
+
Element code or None if out of bounds
|
715
|
+
"""
|
716
|
+
if element_map is not None:
|
717
|
+
if 0 <= y < element_map.shape[0] and 0 <= x < element_map.shape[1]:
|
718
|
+
return element_map[y, x]
|
719
|
+
return None
|
720
|
+
|
721
|
+
|
722
|
+
def get_room_at_position(element_map, x, y, room_base=101):
|
723
|
+
"""
|
724
|
+
Get the room ID at a specific position, or None if not a room.
|
725
|
+
|
726
|
+
Args:
|
727
|
+
element_map: The element map array
|
728
|
+
x: X coordinate
|
729
|
+
y: Y coordinate
|
730
|
+
room_base: Base value for room elements (default: 101 for DrawableElement.ROOM_1)
|
731
|
+
|
732
|
+
Returns:
|
733
|
+
Room ID (1-15) or None if not a room
|
734
|
+
"""
|
735
|
+
element = get_element_at_position(element_map, x, y)
|
736
|
+
if element is not None and room_base <= element <= room_base + 14: # 15 rooms max
|
737
|
+
return element - room_base + 1
|
738
|
+
return None
|
739
|
+
|
740
|
+
|
741
|
+
def update_element_map_with_robot(element_map, robot_position, robot_element=3, robot_radius=25):
|
742
|
+
"""
|
743
|
+
Update the element map with the robot position.
|
744
|
+
|
745
|
+
Args:
|
746
|
+
element_map: The element map to update
|
747
|
+
robot_position: Tuple of (x, y) coordinates for the robot
|
748
|
+
robot_element: Element code for the robot (default: 3 for DrawableElement.ROBOT)
|
749
|
+
robot_radius: Radius of the robot in pixels
|
750
|
+
|
751
|
+
Returns:
|
752
|
+
None
|
753
|
+
"""
|
754
|
+
if element_map is None or robot_position is None:
|
755
|
+
return
|
756
|
+
|
757
|
+
# Update element map for robot position
|
758
|
+
for dy in range(-robot_radius, robot_radius + 1):
|
759
|
+
for dx in range(-robot_radius, robot_radius + 1):
|
760
|
+
if dx * dx + dy * dy <= robot_radius * robot_radius:
|
761
|
+
rx, ry = (
|
762
|
+
int(robot_position[0] + dx),
|
763
|
+
int(robot_position[1] + dy),
|
764
|
+
)
|
765
|
+
if (
|
766
|
+
0 <= ry < element_map.shape[0]
|
767
|
+
and 0 <= rx < element_map.shape[1]
|
768
|
+
):
|
769
|
+
element_map[ry, rx] = robot_element
|
770
|
+
|
771
|
+
|
772
|
+
def manage_drawable_elements(handler, action, element_code=None, element_codes=None, property_name=None, value=None):
|
773
|
+
"""
|
774
|
+
Manage drawable elements (enable, disable, set elements, set properties).
|
775
|
+
|
776
|
+
Args:
|
777
|
+
handler: The handler instance with drawing_config attribute
|
778
|
+
action: Action to perform ('enable', 'disable', 'set_elements', 'set_property')
|
779
|
+
element_code: Element code for enable/disable/set_property actions
|
780
|
+
element_codes: List of element codes for set_elements action
|
781
|
+
property_name: Property name for set_property action
|
782
|
+
value: Property value for set_property action
|
783
|
+
|
784
|
+
Returns:
|
785
|
+
None
|
786
|
+
"""
|
787
|
+
if not hasattr(handler, "drawing_config") or handler.drawing_config is None:
|
788
|
+
return
|
789
|
+
|
790
|
+
if action == "enable" and element_code is not None:
|
791
|
+
handler.drawing_config.enable_element(element_code)
|
792
|
+
elif action == "disable" and element_code is not None:
|
793
|
+
handler.drawing_config.disable_element(element_code)
|
794
|
+
elif action == "set_elements" and element_codes is not None:
|
795
|
+
handler.drawing_config.set_elements(element_codes)
|
796
|
+
elif action == "set_property" and element_code is not None and property_name is not None:
|
797
|
+
handler.drawing_config.set_property(element_code, property_name, value)
|
798
|
+
|
799
|
+
|
800
|
+
def handle_room_outline_error(file_name, room_id, error, logger=None):
|
801
|
+
"""
|
802
|
+
Handle errors during room outline extraction.
|
803
|
+
|
804
|
+
Args:
|
805
|
+
file_name: Name of the file for logging
|
806
|
+
room_id: Room ID for logging
|
807
|
+
error: The error that occurred
|
808
|
+
logger: Logger instance (optional)
|
809
|
+
|
810
|
+
Returns:
|
811
|
+
None
|
812
|
+
"""
|
813
|
+
_LOGGER = logger or logging.getLogger(__name__)
|
814
|
+
|
815
|
+
_LOGGER.warning(
|
816
|
+
"%s: Failed to trace outline for room %s: %s",
|
817
|
+
file_name, str(room_id), str(error)
|
818
|
+
)
|
819
|
+
|
820
|
+
|
821
|
+
async def async_extract_room_outline(room_mask, min_x, min_y, max_x, max_y, file_name, room_id_int, logger=None):
|
822
|
+
"""
|
823
|
+
Extract the outline of a room from a binary mask.
|
824
|
+
|
825
|
+
Args:
|
826
|
+
room_mask: Binary mask where room pixels are 1 and non-room pixels are 0
|
827
|
+
min_x: Minimum x coordinate of the room
|
828
|
+
min_y: Minimum y coordinate of the room
|
829
|
+
max_x: Maximum x coordinate of the room
|
830
|
+
max_y: Maximum y coordinate of the room
|
831
|
+
file_name: Name of the file for logging
|
832
|
+
room_id_int: Room ID for logging
|
833
|
+
logger: Logger instance (optional)
|
834
|
+
|
835
|
+
Returns:
|
836
|
+
List of (x, y) points forming the room outline
|
837
|
+
"""
|
838
|
+
# Use the provided logger or create a new one
|
839
|
+
_LOGGER = logger or logging.getLogger(__name__)
|
840
|
+
|
841
|
+
# Get the dimensions of the mask
|
842
|
+
height, width = room_mask.shape
|
843
|
+
|
844
|
+
# Find the coordinates of all room pixels
|
845
|
+
room_y, room_x = np.where(room_mask > 0)
|
846
|
+
if len(room_y) == 0:
|
847
|
+
return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
|
848
|
+
|
849
|
+
# Get the bounding box of the room
|
850
|
+
min_y, max_y = np.min(room_y), np.max(room_y)
|
851
|
+
min_x, max_x = np.min(room_x), np.max(room_x)
|
852
|
+
|
853
|
+
# For simple rooms, just use the rectangular outline
|
854
|
+
rect_outline = [
|
855
|
+
(min_x, min_y), # Top-left
|
856
|
+
(max_x, min_y), # Top-right
|
857
|
+
(max_x, max_y), # Bottom-right
|
858
|
+
(min_x, max_y), # Bottom-left
|
859
|
+
]
|
860
|
+
|
861
|
+
# For more complex room shapes, trace the boundary
|
862
|
+
# This is a custom boundary tracing algorithm that works without OpenCV
|
863
|
+
try:
|
864
|
+
# Create a padded mask to handle edge cases
|
865
|
+
padded_mask = np.zeros((height + 2, width + 2), dtype=np.uint8)
|
866
|
+
padded_mask[1:-1, 1:-1] = room_mask
|
867
|
+
|
868
|
+
# Find boundary pixels (pixels that have at least one non-room neighbor)
|
869
|
+
boundary_points = []
|
870
|
+
|
871
|
+
# More efficient boundary detection - only check pixels that are part of the room
|
872
|
+
for y, x in zip(room_y, room_x):
|
873
|
+
# Check if this is a boundary pixel (at least one neighbor is 0)
|
874
|
+
is_boundary = False
|
875
|
+
for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
876
|
+
ny, nx = y + dy, x + dx
|
877
|
+
if (ny < 0 or ny >= height or nx < 0 or nx >= width or
|
878
|
+
room_mask[ny, nx] == 0):
|
879
|
+
is_boundary = True
|
880
|
+
break
|
881
|
+
if is_boundary:
|
882
|
+
boundary_points.append((x, y))
|
883
|
+
|
884
|
+
# Log the number of boundary points found
|
885
|
+
_LOGGER.debug(
|
886
|
+
"%s: Room %s has %d boundary points",
|
887
|
+
file_name, str(room_id_int), len(boundary_points)
|
888
|
+
)
|
889
|
+
|
890
|
+
# If we found too few boundary points, use the rectangular outline
|
891
|
+
if len(boundary_points) < 8: # Need at least 8 points for a meaningful shape
|
892
|
+
_LOGGER.debug(
|
893
|
+
"%s: Room %s has too few boundary points (%d), using rectangular outline",
|
894
|
+
file_name, str(room_id_int), len(boundary_points)
|
895
|
+
)
|
896
|
+
return rect_outline
|
897
|
+
|
898
|
+
# Use a more sophisticated algorithm to create a coherent outline
|
899
|
+
# We'll use a convex hull approach to get the main shape
|
900
|
+
# Sort points by angle from centroid
|
901
|
+
centroid_x = np.mean([p[0] for p in boundary_points])
|
902
|
+
centroid_y = np.mean([p[1] for p in boundary_points])
|
903
|
+
|
904
|
+
# Calculate angles from centroid
|
905
|
+
def calculate_angle(point):
|
906
|
+
return np.arctan2(point[1] - centroid_y, point[0] - centroid_x)
|
907
|
+
|
908
|
+
# Sort boundary points by angle
|
909
|
+
boundary_points.sort(key=calculate_angle)
|
910
|
+
|
911
|
+
# Simplify the outline if it has too many points
|
912
|
+
if len(boundary_points) > 20:
|
913
|
+
# Take every Nth point to simplify
|
914
|
+
step = len(boundary_points) // 20
|
915
|
+
simplified_outline = [boundary_points[i] for i in range(0, len(boundary_points), step)]
|
916
|
+
# Make sure we have at least 8 points
|
917
|
+
if len(simplified_outline) < 8:
|
918
|
+
simplified_outline = boundary_points[::len(boundary_points)//8]
|
919
|
+
else:
|
920
|
+
simplified_outline = boundary_points
|
921
|
+
|
922
|
+
# Make sure to close the loop
|
923
|
+
if simplified_outline[0] != simplified_outline[-1]:
|
924
|
+
simplified_outline.append(simplified_outline[0])
|
925
|
+
|
926
|
+
# Convert NumPy int64 values to regular Python integers
|
927
|
+
simplified_outline = [(int(x), int(y)) for x, y in simplified_outline]
|
928
|
+
|
929
|
+
_LOGGER.debug(
|
930
|
+
"%s: Room %s outline has %d points",
|
931
|
+
file_name, str(room_id_int), len(simplified_outline)
|
932
|
+
)
|
933
|
+
|
934
|
+
return simplified_outline
|
935
|
+
|
936
|
+
except (ValueError, IndexError, TypeError, ArithmeticError) as e:
|
937
|
+
_LOGGER.warning(
|
938
|
+
"%s: Error tracing room outline: %s. Using rectangular outline instead.",
|
939
|
+
file_name, str(e)
|
940
|
+
)
|
941
|
+
return rect_outline
|