neuro-sam 0.1.0__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.
- neuro_sam/__init__.py +1 -0
- neuro_sam/brightest_path_lib/__init__.py +5 -0
- neuro_sam/brightest_path_lib/algorithm/__init__.py +3 -0
- neuro_sam/brightest_path_lib/algorithm/astar.py +586 -0
- neuro_sam/brightest_path_lib/algorithm/waypointastar.py +449 -0
- neuro_sam/brightest_path_lib/algorithm/waypointastar_speedup.py +1007 -0
- neuro_sam/brightest_path_lib/connected_componen.py +329 -0
- neuro_sam/brightest_path_lib/cost/__init__.py +8 -0
- neuro_sam/brightest_path_lib/cost/cost.py +33 -0
- neuro_sam/brightest_path_lib/cost/reciprocal.py +90 -0
- neuro_sam/brightest_path_lib/cost/reciprocal_transonic.py +86 -0
- neuro_sam/brightest_path_lib/heuristic/__init__.py +2 -0
- neuro_sam/brightest_path_lib/heuristic/euclidean.py +101 -0
- neuro_sam/brightest_path_lib/heuristic/heuristic.py +29 -0
- neuro_sam/brightest_path_lib/image/__init__.py +1 -0
- neuro_sam/brightest_path_lib/image/stats.py +197 -0
- neuro_sam/brightest_path_lib/input/__init__.py +1 -0
- neuro_sam/brightest_path_lib/input/inputs.py +14 -0
- neuro_sam/brightest_path_lib/node/__init__.py +2 -0
- neuro_sam/brightest_path_lib/node/bidirectional_node.py +240 -0
- neuro_sam/brightest_path_lib/node/node.py +125 -0
- neuro_sam/brightest_path_lib/visualization/__init__.py +4 -0
- neuro_sam/brightest_path_lib/visualization/flythrough.py +133 -0
- neuro_sam/brightest_path_lib/visualization/flythrough_all.py +394 -0
- neuro_sam/brightest_path_lib/visualization/tube_data.py +385 -0
- neuro_sam/brightest_path_lib/visualization/tube_flythrough.py +227 -0
- neuro_sam/napari_utils/anisotropic_scaling.py +503 -0
- neuro_sam/napari_utils/color_utils.py +135 -0
- neuro_sam/napari_utils/contrasting_color_system.py +169 -0
- neuro_sam/napari_utils/main_widget.py +1016 -0
- neuro_sam/napari_utils/path_tracing_module.py +1016 -0
- neuro_sam/napari_utils/punet_widget.py +424 -0
- neuro_sam/napari_utils/segmentation_model.py +769 -0
- neuro_sam/napari_utils/segmentation_module.py +649 -0
- neuro_sam/napari_utils/visualization_module.py +574 -0
- neuro_sam/plugin.py +260 -0
- neuro_sam/punet/__init__.py +0 -0
- neuro_sam/punet/deepd3_model.py +231 -0
- neuro_sam/punet/prob_unet_deepd3.py +431 -0
- neuro_sam/punet/prob_unet_with_tversky.py +375 -0
- neuro_sam/punet/punet_inference.py +236 -0
- neuro_sam/punet/run_inference.py +145 -0
- neuro_sam/punet/unet_blocks.py +81 -0
- neuro_sam/punet/utils.py +52 -0
- neuro_sam-0.1.0.dist-info/METADATA +269 -0
- neuro_sam-0.1.0.dist-info/RECORD +93 -0
- neuro_sam-0.1.0.dist-info/WHEEL +5 -0
- neuro_sam-0.1.0.dist-info/entry_points.txt +2 -0
- neuro_sam-0.1.0.dist-info/licenses/LICENSE +21 -0
- neuro_sam-0.1.0.dist-info/top_level.txt +2 -0
- sam2/__init__.py +11 -0
- sam2/automatic_mask_generator.py +454 -0
- sam2/benchmark.py +92 -0
- sam2/build_sam.py +174 -0
- sam2/configs/sam2/sam2_hiera_b+.yaml +113 -0
- sam2/configs/sam2/sam2_hiera_l.yaml +117 -0
- sam2/configs/sam2/sam2_hiera_s.yaml +116 -0
- sam2/configs/sam2/sam2_hiera_t.yaml +118 -0
- sam2/configs/sam2.1/sam2.1_hiera_b+.yaml +116 -0
- sam2/configs/sam2.1/sam2.1_hiera_l.yaml +120 -0
- sam2/configs/sam2.1/sam2.1_hiera_s.yaml +119 -0
- sam2/configs/sam2.1/sam2.1_hiera_t.yaml +121 -0
- sam2/configs/sam2.1_training/sam2.1_hiera_b+_MOSE_finetune.yaml +339 -0
- sam2/configs/train.yaml +335 -0
- sam2/modeling/__init__.py +5 -0
- sam2/modeling/backbones/__init__.py +5 -0
- sam2/modeling/backbones/hieradet.py +317 -0
- sam2/modeling/backbones/image_encoder.py +134 -0
- sam2/modeling/backbones/utils.py +93 -0
- sam2/modeling/memory_attention.py +169 -0
- sam2/modeling/memory_encoder.py +181 -0
- sam2/modeling/position_encoding.py +239 -0
- sam2/modeling/sam/__init__.py +5 -0
- sam2/modeling/sam/mask_decoder.py +295 -0
- sam2/modeling/sam/prompt_encoder.py +202 -0
- sam2/modeling/sam/transformer.py +311 -0
- sam2/modeling/sam2_base.py +911 -0
- sam2/modeling/sam2_utils.py +323 -0
- sam2/sam2.1_hiera_b+.yaml +116 -0
- sam2/sam2.1_hiera_l.yaml +120 -0
- sam2/sam2.1_hiera_s.yaml +119 -0
- sam2/sam2.1_hiera_t.yaml +121 -0
- sam2/sam2_hiera_b+.yaml +113 -0
- sam2/sam2_hiera_l.yaml +117 -0
- sam2/sam2_hiera_s.yaml +116 -0
- sam2/sam2_hiera_t.yaml +118 -0
- sam2/sam2_image_predictor.py +475 -0
- sam2/sam2_video_predictor.py +1222 -0
- sam2/sam2_video_predictor_legacy.py +1172 -0
- sam2/utils/__init__.py +5 -0
- sam2/utils/amg.py +348 -0
- sam2/utils/misc.py +349 -0
- sam2/utils/transforms.py +118 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""Advanced optimized A* search implementation with waypoint support for finding brightest paths.
|
|
2
|
+
This extends the BidirectionalAStarSearch to support user-defined auxiliary waypoints with
|
|
3
|
+
performance optimizations matching the core algorithm.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import heapq
|
|
7
|
+
import math
|
|
8
|
+
import numpy as np
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from typing import List, Tuple, Dict, Set, Any, Optional
|
|
11
|
+
import numba as nb
|
|
12
|
+
from numba import njit, prange, jit
|
|
13
|
+
|
|
14
|
+
from neuro_sam.brightest_path_lib.algorithm.astar import (
|
|
15
|
+
BidirectionalAStarSearch, array_equal, euclidean_distance_scaled,
|
|
16
|
+
find_2D_neighbors_optimized, find_3D_neighbors_optimized
|
|
17
|
+
)
|
|
18
|
+
from neuro_sam.brightest_path_lib.cost import Reciprocal
|
|
19
|
+
from neuro_sam.brightest_path_lib.heuristic import Euclidean
|
|
20
|
+
from neuro_sam.brightest_path_lib.image import ImageStats
|
|
21
|
+
from neuro_sam.brightest_path_lib.input import CostFunction, HeuristicFunction
|
|
22
|
+
from neuro_sam.brightest_path_lib.node import Node
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Numba-optimized function to check if all points are on the same z-plane
|
|
26
|
+
@nb.njit(cache=True)
|
|
27
|
+
def check_same_z_plane(start_point, goal_point, waypoints):
|
|
28
|
+
"""Check if all points are on the same z-plane for 3D optimization"""
|
|
29
|
+
# If points are 2D, return True
|
|
30
|
+
if len(start_point) == 2:
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
# For 3D points, check z-coordinates
|
|
34
|
+
if len(start_point) == 3:
|
|
35
|
+
z_val = start_point[0]
|
|
36
|
+
|
|
37
|
+
# Check if goal has same z
|
|
38
|
+
if goal_point[0] != z_val:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
# Check all waypoints
|
|
42
|
+
for i in range(len(waypoints)):
|
|
43
|
+
if waypoints[i][0] != z_val:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
# All points have same z
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
# For higher dimensions, don't use the optimization
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Numba-optimized function to create 2D points from 3D points
|
|
54
|
+
@nb.njit(cache=True)
|
|
55
|
+
def convert_3d_to_2d_points(start_point, goal_point, waypoints):
|
|
56
|
+
"""Convert 3D points to 2D by removing z-coordinate"""
|
|
57
|
+
start_2d = np.array([start_point[1], start_point[2]], dtype=np.int32)
|
|
58
|
+
goal_2d = np.array([goal_point[1], goal_point[2]], dtype=np.int32)
|
|
59
|
+
|
|
60
|
+
# Convert waypoints
|
|
61
|
+
waypoints_2d = np.empty((len(waypoints), 2), dtype=np.int32)
|
|
62
|
+
for i in range(len(waypoints)):
|
|
63
|
+
waypoints_2d[i, 0] = waypoints[i][1]
|
|
64
|
+
waypoints_2d[i, 1] = waypoints[i][2]
|
|
65
|
+
|
|
66
|
+
return start_2d, goal_2d, waypoints_2d
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Numba-optimized function to convert 2D path back to 3D
|
|
70
|
+
@nb.njit(cache=True)
|
|
71
|
+
def convert_2d_path_to_3d(path_2d, z_value):
|
|
72
|
+
"""Convert a 2D path back to 3D by adding z-coordinate"""
|
|
73
|
+
path_3d = np.empty((len(path_2d), 3), dtype=np.int32)
|
|
74
|
+
|
|
75
|
+
for i in range(len(path_2d)):
|
|
76
|
+
path_3d[i, 0] = z_value
|
|
77
|
+
path_3d[i, 1] = path_2d[i][0]
|
|
78
|
+
path_3d[i, 2] = path_2d[i][1]
|
|
79
|
+
|
|
80
|
+
return path_3d
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class WaypointBidirectionalAStarSearch:
|
|
84
|
+
"""Advanced bidirectional A* search implementation with waypoint support
|
|
85
|
+
|
|
86
|
+
This implementation allows users to specify intermediate points that
|
|
87
|
+
the path should pass through, breaking down a complex search into
|
|
88
|
+
multiple simpler searches between consecutive points.
|
|
89
|
+
|
|
90
|
+
Performance optimization: When waypoints are on the same z-plane,
|
|
91
|
+
the algorithm automatically uses 2D pathfinding instead of 3D.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
image: np.ndarray,
|
|
97
|
+
start_point: np.ndarray,
|
|
98
|
+
goal_point: np.ndarray,
|
|
99
|
+
waypoints: List[np.ndarray] = None,
|
|
100
|
+
scale: Tuple = (1.0, 1.0),
|
|
101
|
+
cost_function: CostFunction = CostFunction.RECIPROCAL,
|
|
102
|
+
heuristic_function: HeuristicFunction = HeuristicFunction.EUCLIDEAN,
|
|
103
|
+
open_nodes=None,
|
|
104
|
+
use_hierarchical: bool = False,
|
|
105
|
+
weight_heuristic: float = 1.0
|
|
106
|
+
):
|
|
107
|
+
"""Initialize waypoint-enabled A* search
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
image : numpy ndarray
|
|
112
|
+
The image to search
|
|
113
|
+
start_point, goal_point : numpy ndarray
|
|
114
|
+
Start and goal coordinates
|
|
115
|
+
waypoints : List[numpy ndarray], optional
|
|
116
|
+
List of intermediate points that the path must pass through,
|
|
117
|
+
in the order they should be visited
|
|
118
|
+
scale : tuple
|
|
119
|
+
Image scale factors
|
|
120
|
+
cost_function, heuristic_function : Enum
|
|
121
|
+
Functions to use for cost and heuristic
|
|
122
|
+
open_nodes : Queue, optional
|
|
123
|
+
Queue for visualization
|
|
124
|
+
use_hierarchical : bool
|
|
125
|
+
Whether to use hierarchical search for large images
|
|
126
|
+
weight_heuristic : float
|
|
127
|
+
Weight for heuristic (> 1.0 makes search faster but less optimal)
|
|
128
|
+
"""
|
|
129
|
+
self._validate_inputs(image, start_point, goal_point, waypoints)
|
|
130
|
+
|
|
131
|
+
# Basic parameters
|
|
132
|
+
self.image = image
|
|
133
|
+
self.image_stats = ImageStats(image)
|
|
134
|
+
self.start_point = np.round(start_point).astype(np.int32)
|
|
135
|
+
self.goal_point = np.round(goal_point).astype(np.int32)
|
|
136
|
+
|
|
137
|
+
# Process and validate waypoints if provided
|
|
138
|
+
if waypoints and len(waypoints) > 0:
|
|
139
|
+
self.waypoints = np.array([np.round(wp).astype(np.int32) for wp in waypoints])
|
|
140
|
+
else:
|
|
141
|
+
# Empty array with correct shape for numba compatibility
|
|
142
|
+
self.waypoints = np.empty((0, len(self.start_point)), dtype=np.int32)
|
|
143
|
+
|
|
144
|
+
self.scale = scale
|
|
145
|
+
self.open_nodes = open_nodes
|
|
146
|
+
self.use_hierarchical = use_hierarchical
|
|
147
|
+
self.weight_heuristic = weight_heuristic
|
|
148
|
+
|
|
149
|
+
if cost_function == CostFunction.RECIPROCAL:
|
|
150
|
+
self.cost_function = Reciprocal(
|
|
151
|
+
min_intensity=self.image_stats.min_intensity,
|
|
152
|
+
max_intensity=self.image_stats.max_intensity)
|
|
153
|
+
|
|
154
|
+
if heuristic_function == HeuristicFunction.EUCLIDEAN:
|
|
155
|
+
self.heuristic_function = Euclidean(scale=self.scale)
|
|
156
|
+
|
|
157
|
+
# State variables
|
|
158
|
+
self.is_canceled = False
|
|
159
|
+
self.found_path = False
|
|
160
|
+
self.evaluated_nodes = 0
|
|
161
|
+
self.result = []
|
|
162
|
+
self.segment_results = [] # Store individual segment paths
|
|
163
|
+
self.segment_evaluated_nodes = [] # Track nodes evaluated per segment
|
|
164
|
+
|
|
165
|
+
# Determine if we should use 2D mode (all points have same z-coordinate)
|
|
166
|
+
self.use_2d_mode = check_same_z_plane(
|
|
167
|
+
self.start_point, self.goal_point, self.waypoints)
|
|
168
|
+
|
|
169
|
+
# For 2D optimization when applicable
|
|
170
|
+
self.z_value = self.start_point[0] if len(self.start_point) == 3 else None
|
|
171
|
+
|
|
172
|
+
def _validate_inputs(
|
|
173
|
+
self,
|
|
174
|
+
image: np.ndarray,
|
|
175
|
+
start_point: np.ndarray,
|
|
176
|
+
goal_point: np.ndarray,
|
|
177
|
+
waypoints: Optional[List[np.ndarray]] = None
|
|
178
|
+
):
|
|
179
|
+
"""Validate input parameters"""
|
|
180
|
+
if image is None or start_point is None or goal_point is None:
|
|
181
|
+
raise TypeError("Image, start_point, and goal_point cannot be None")
|
|
182
|
+
if len(image) == 0 or len(start_point) == 0 or len(goal_point) == 0:
|
|
183
|
+
raise ValueError("Image, start_point, and goal_point cannot be empty")
|
|
184
|
+
|
|
185
|
+
# Validate waypoints if provided
|
|
186
|
+
if waypoints:
|
|
187
|
+
for i, wp in enumerate(waypoints):
|
|
188
|
+
if wp is None:
|
|
189
|
+
raise TypeError(f"Waypoint {i} cannot be None")
|
|
190
|
+
if len(wp) == 0:
|
|
191
|
+
raise ValueError(f"Waypoint {i} cannot be empty")
|
|
192
|
+
if len(wp) != len(start_point):
|
|
193
|
+
raise ValueError(f"Waypoint {i} dimensions must match start_point dimensions")
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def found_path(self) -> bool:
|
|
197
|
+
return self._found_path
|
|
198
|
+
|
|
199
|
+
@found_path.setter
|
|
200
|
+
def found_path(self, value: bool):
|
|
201
|
+
if value is None:
|
|
202
|
+
raise TypeError
|
|
203
|
+
self._found_path = value
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def is_canceled(self) -> bool:
|
|
207
|
+
return self._is_canceled
|
|
208
|
+
|
|
209
|
+
@is_canceled.setter
|
|
210
|
+
def is_canceled(self, value: bool):
|
|
211
|
+
if value is None:
|
|
212
|
+
raise TypeError
|
|
213
|
+
self._is_canceled = value
|
|
214
|
+
|
|
215
|
+
def search(self, verbose: bool = False) -> List[np.ndarray]:
|
|
216
|
+
"""Perform A* search with waypoints
|
|
217
|
+
|
|
218
|
+
This method breaks down the search into multiple segments:
|
|
219
|
+
start→waypoint₁, waypoint₁→waypoint₂, ..., waypointₙ→goal
|
|
220
|
+
|
|
221
|
+
Performance optimization: Uses 2D mode when all points are on the same z-plane
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
List[np.ndarray]
|
|
226
|
+
Complete path from start to goal through all waypoints
|
|
227
|
+
"""
|
|
228
|
+
# Reset state
|
|
229
|
+
self.result = []
|
|
230
|
+
self.segment_results = []
|
|
231
|
+
self.segment_evaluated_nodes = []
|
|
232
|
+
self.evaluated_nodes = 0
|
|
233
|
+
|
|
234
|
+
# Check if we can use 2D mode for performance optimization
|
|
235
|
+
if self.use_2d_mode and len(self.start_point) == 3:
|
|
236
|
+
if verbose:
|
|
237
|
+
print("Using 2D mode optimization (all points on same z-plane)")
|
|
238
|
+
return self._search_2d_mode(verbose)
|
|
239
|
+
else:
|
|
240
|
+
return self._search_normal_mode(verbose)
|
|
241
|
+
|
|
242
|
+
def _search_2d_mode(self, verbose: bool = False) -> List[np.ndarray]:
|
|
243
|
+
"""Perform search in 2D mode for performance optimization
|
|
244
|
+
|
|
245
|
+
This extracts a 2D slice from the 3D image when all points are on the same z-plane
|
|
246
|
+
"""
|
|
247
|
+
# Extract 2D slice and convert points to 2D
|
|
248
|
+
image_2d = self.image[self.z_value]
|
|
249
|
+
start_2d, goal_2d, waypoints_2d = convert_3d_to_2d_points(
|
|
250
|
+
self.start_point, self.goal_point, self.waypoints)
|
|
251
|
+
|
|
252
|
+
# Create a list of all points in order, including start and goal
|
|
253
|
+
wp_list = [waypoints_2d[i] for i in range(len(waypoints_2d))]
|
|
254
|
+
all_points_2d = [start_2d] + wp_list + [goal_2d]
|
|
255
|
+
|
|
256
|
+
# Track overall success
|
|
257
|
+
overall_success = True
|
|
258
|
+
|
|
259
|
+
if verbose:
|
|
260
|
+
if len(wp_list) > 0:
|
|
261
|
+
print(f"Starting 2D search with {len(wp_list)} waypoints")
|
|
262
|
+
else:
|
|
263
|
+
print("Starting 2D search (no waypoints)")
|
|
264
|
+
|
|
265
|
+
# Process each segment (start to first waypoint, waypoint to waypoint, last waypoint to goal)
|
|
266
|
+
for i in range(len(all_points_2d) - 1):
|
|
267
|
+
if self.is_canceled:
|
|
268
|
+
return []
|
|
269
|
+
|
|
270
|
+
point_a = all_points_2d[i]
|
|
271
|
+
point_b = all_points_2d[i+1]
|
|
272
|
+
|
|
273
|
+
if verbose:
|
|
274
|
+
if i == 0:
|
|
275
|
+
print(f"Searching from start to {'waypoint' if i+1 < len(all_points_2d)-1 else 'goal'} {i+1}")
|
|
276
|
+
elif i+1 == len(all_points_2d)-1:
|
|
277
|
+
print(f"Searching from waypoint {i} to goal")
|
|
278
|
+
else:
|
|
279
|
+
print(f"Searching from waypoint {i} to waypoint {i+1}")
|
|
280
|
+
|
|
281
|
+
# Create A* search for this segment
|
|
282
|
+
segment_search = BidirectionalAStarSearch(
|
|
283
|
+
image=image_2d,
|
|
284
|
+
start_point=point_a,
|
|
285
|
+
goal_point=point_b,
|
|
286
|
+
scale=self.scale[:2], # Only use x,y scale
|
|
287
|
+
cost_function=CostFunction.RECIPROCAL,
|
|
288
|
+
heuristic_function=HeuristicFunction.EUCLIDEAN,
|
|
289
|
+
open_nodes=self.open_nodes,
|
|
290
|
+
use_hierarchical=self.use_hierarchical,
|
|
291
|
+
weight_heuristic=self.weight_heuristic
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Run the search for this segment
|
|
295
|
+
segment_path_2d = segment_search.search(verbose=verbose)
|
|
296
|
+
|
|
297
|
+
# Track stats and results
|
|
298
|
+
self.segment_evaluated_nodes.append(segment_search.evaluated_nodes)
|
|
299
|
+
self.evaluated_nodes += segment_search.evaluated_nodes
|
|
300
|
+
|
|
301
|
+
# Check if segment search was successful
|
|
302
|
+
if segment_search.found_path and len(segment_path_2d) > 0:
|
|
303
|
+
# Convert 2D path back to 3D
|
|
304
|
+
segment_path_3d = convert_2d_path_to_3d(segment_path_2d, self.z_value)
|
|
305
|
+
# Store as list of arrays for compatibility
|
|
306
|
+
segment_path_list = [segment_path_3d[i] for i in range(len(segment_path_3d))]
|
|
307
|
+
self.segment_results.append(segment_path_list)
|
|
308
|
+
|
|
309
|
+
if verbose:
|
|
310
|
+
print(f"Found path for segment {i+1} ({len(segment_path_2d)} points, {segment_search.evaluated_nodes} nodes evaluated)")
|
|
311
|
+
else:
|
|
312
|
+
if verbose:
|
|
313
|
+
print(f"Failed to find path for segment {i+1}")
|
|
314
|
+
overall_success = False
|
|
315
|
+
break
|
|
316
|
+
|
|
317
|
+
# If all segments were successful, combine the paths
|
|
318
|
+
if overall_success:
|
|
319
|
+
self._construct_complete_path()
|
|
320
|
+
self.found_path = True
|
|
321
|
+
|
|
322
|
+
if verbose:
|
|
323
|
+
print(f"Complete path found: {len(self.result)} points, {self.evaluated_nodes} total nodes evaluated")
|
|
324
|
+
else:
|
|
325
|
+
if verbose:
|
|
326
|
+
print("Failed to find a complete path through all waypoints")
|
|
327
|
+
|
|
328
|
+
return self.result
|
|
329
|
+
|
|
330
|
+
def _search_normal_mode(self, verbose: bool = False) -> List[np.ndarray]:
|
|
331
|
+
"""Perform search in normal mode (without special 2D optimization)"""
|
|
332
|
+
# Create a list of all points in order, including start and goal
|
|
333
|
+
wp_list = [self.waypoints[i] for i in range(len(self.waypoints))]
|
|
334
|
+
all_points = [self.start_point] + wp_list + [self.goal_point]
|
|
335
|
+
|
|
336
|
+
# Track overall success
|
|
337
|
+
overall_success = True
|
|
338
|
+
|
|
339
|
+
if verbose:
|
|
340
|
+
if len(wp_list) > 0:
|
|
341
|
+
print(f"Starting search with {len(wp_list)} waypoints")
|
|
342
|
+
else:
|
|
343
|
+
print("Starting search (no waypoints)")
|
|
344
|
+
|
|
345
|
+
# Process each segment (start to first waypoint, waypoint to waypoint, last waypoint to goal)
|
|
346
|
+
for i in range(len(all_points) - 1):
|
|
347
|
+
if self.is_canceled:
|
|
348
|
+
return []
|
|
349
|
+
|
|
350
|
+
point_a = all_points[i]
|
|
351
|
+
point_b = all_points[i+1]
|
|
352
|
+
|
|
353
|
+
if verbose:
|
|
354
|
+
if i == 0:
|
|
355
|
+
print(f"Searching from start to {'waypoint' if i+1 < len(all_points)-1 else 'goal'} {i+1}")
|
|
356
|
+
elif i+1 == len(all_points)-1:
|
|
357
|
+
print(f"Searching from waypoint {i} to goal")
|
|
358
|
+
else:
|
|
359
|
+
print(f"Searching from waypoint {i} to waypoint {i+1}")
|
|
360
|
+
|
|
361
|
+
# Create A* search for this segment
|
|
362
|
+
segment_search = BidirectionalAStarSearch(
|
|
363
|
+
image=self.image,
|
|
364
|
+
start_point=point_a,
|
|
365
|
+
goal_point=point_b,
|
|
366
|
+
scale=self.scale,
|
|
367
|
+
cost_function=CostFunction.RECIPROCAL,
|
|
368
|
+
heuristic_function=HeuristicFunction.EUCLIDEAN,
|
|
369
|
+
open_nodes=self.open_nodes,
|
|
370
|
+
use_hierarchical=self.use_hierarchical,
|
|
371
|
+
weight_heuristic=self.weight_heuristic
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Run the search for this segment
|
|
375
|
+
segment_path = segment_search.search(verbose=verbose)
|
|
376
|
+
|
|
377
|
+
# Track stats and results
|
|
378
|
+
self.segment_evaluated_nodes.append(segment_search.evaluated_nodes)
|
|
379
|
+
self.evaluated_nodes += segment_search.evaluated_nodes
|
|
380
|
+
|
|
381
|
+
# Check if segment search was successful
|
|
382
|
+
if segment_search.found_path and len(segment_path) > 0:
|
|
383
|
+
self.segment_results.append(segment_path)
|
|
384
|
+
|
|
385
|
+
if verbose:
|
|
386
|
+
print(f"Found path for segment {i+1} ({len(segment_path)} points, {segment_search.evaluated_nodes} nodes evaluated)")
|
|
387
|
+
else:
|
|
388
|
+
if verbose:
|
|
389
|
+
print(f"Failed to find path for segment {i+1}")
|
|
390
|
+
overall_success = False
|
|
391
|
+
break
|
|
392
|
+
|
|
393
|
+
# If all segments were successful, combine the paths
|
|
394
|
+
if overall_success:
|
|
395
|
+
self._construct_complete_path()
|
|
396
|
+
self.found_path = True
|
|
397
|
+
|
|
398
|
+
if verbose:
|
|
399
|
+
print(f"Complete path found: {len(self.result)} points, {self.evaluated_nodes} total nodes evaluated")
|
|
400
|
+
else:
|
|
401
|
+
if verbose:
|
|
402
|
+
print("Failed to find a complete path through all waypoints")
|
|
403
|
+
|
|
404
|
+
return self.result
|
|
405
|
+
|
|
406
|
+
def _construct_complete_path(self):
|
|
407
|
+
"""Combine segment paths into a complete path
|
|
408
|
+
|
|
409
|
+
This handles removing duplicate points at segment boundaries.
|
|
410
|
+
"""
|
|
411
|
+
if not self.segment_results:
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
# Start with the first segment
|
|
415
|
+
self.result = self.segment_results[0].copy()
|
|
416
|
+
|
|
417
|
+
# Add each subsequent segment (skipping the first point to avoid duplication)
|
|
418
|
+
for i in range(1, len(self.segment_results)):
|
|
419
|
+
segment = self.segment_results[i]
|
|
420
|
+
|
|
421
|
+
# Skip the first point of each subsequent segment as it should
|
|
422
|
+
# be the same as the last point of the previous segment
|
|
423
|
+
# But verify they're actually the same
|
|
424
|
+
if len(segment) > 1 and np.array_equal(self.result[-1], segment[0]):
|
|
425
|
+
self.result.extend(segment[1:])
|
|
426
|
+
else:
|
|
427
|
+
# Something's wrong - just append everything
|
|
428
|
+
self.result.extend(segment)
|
|
429
|
+
|
|
430
|
+
def get_segment_info(self):
|
|
431
|
+
"""Get information about each segment of the path
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
dict
|
|
436
|
+
Dictionary with information about each path segment
|
|
437
|
+
"""
|
|
438
|
+
if not self.segment_results:
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
info = {
|
|
442
|
+
'num_segments': len(self.segment_results),
|
|
443
|
+
'segment_lengths': [len(path) for path in self.segment_results],
|
|
444
|
+
'segment_evaluated_nodes': self.segment_evaluated_nodes,
|
|
445
|
+
'total_path_length': len(self.result),
|
|
446
|
+
'total_evaluated_nodes': self.evaluated_nodes,
|
|
447
|
+
'used_2d_optimization': self.use_2d_mode and len(self.start_point) == 3
|
|
448
|
+
}
|
|
449
|
+
return info
|