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,197 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numba as nb
|
|
3
|
+
|
|
4
|
+
# Numba-optimized functions for faster image stats calculation
|
|
5
|
+
@nb.njit(fastmath=True)
|
|
6
|
+
def compute_image_intensity_range(image):
|
|
7
|
+
"""Efficiently compute min and max intensity of an image using Numba"""
|
|
8
|
+
# Use Numba's optimized implementation rather than np.min/np.max
|
|
9
|
+
# for better performance, especially with large arrays
|
|
10
|
+
|
|
11
|
+
# Initialize with extreme values
|
|
12
|
+
min_val = np.inf
|
|
13
|
+
max_val = -np.inf
|
|
14
|
+
|
|
15
|
+
# For 1D arrays (unlikely but handled for completeness)
|
|
16
|
+
if image.ndim == 1:
|
|
17
|
+
for i in range(image.shape[0]):
|
|
18
|
+
val = image[i]
|
|
19
|
+
if val < min_val:
|
|
20
|
+
min_val = val
|
|
21
|
+
if val > max_val:
|
|
22
|
+
max_val = val
|
|
23
|
+
|
|
24
|
+
# For 2D arrays (most common case)
|
|
25
|
+
elif image.ndim == 2:
|
|
26
|
+
for i in range(image.shape[0]):
|
|
27
|
+
for j in range(image.shape[1]):
|
|
28
|
+
val = image[i, j]
|
|
29
|
+
if val < min_val:
|
|
30
|
+
min_val = val
|
|
31
|
+
if val > max_val:
|
|
32
|
+
max_val = val
|
|
33
|
+
|
|
34
|
+
# For 3D arrays
|
|
35
|
+
elif image.ndim == 3:
|
|
36
|
+
for i in range(image.shape[0]):
|
|
37
|
+
for j in range(image.shape[1]):
|
|
38
|
+
for k in range(image.shape[2]):
|
|
39
|
+
val = image[i, j, k]
|
|
40
|
+
if val < min_val:
|
|
41
|
+
min_val = val
|
|
42
|
+
if val > max_val:
|
|
43
|
+
max_val = val
|
|
44
|
+
|
|
45
|
+
return float(min_val), float(max_val)
|
|
46
|
+
|
|
47
|
+
@nb.njit
|
|
48
|
+
def compute_image_dimensions(image_shape):
|
|
49
|
+
"""Compute image dimensions and coordinate ranges"""
|
|
50
|
+
ndim = len(image_shape)
|
|
51
|
+
|
|
52
|
+
# Initialize with default values
|
|
53
|
+
x_min, y_min, z_min = 0, 0, 0
|
|
54
|
+
x_max, y_max, z_max = 0, 0, 0
|
|
55
|
+
|
|
56
|
+
if ndim == 2: # 2D image
|
|
57
|
+
y_max = image_shape[0] - 1
|
|
58
|
+
x_max = image_shape[1] - 1
|
|
59
|
+
elif ndim == 3: # 3D image
|
|
60
|
+
z_max = image_shape[0] - 1
|
|
61
|
+
y_max = image_shape[1] - 1
|
|
62
|
+
x_max = image_shape[2] - 1
|
|
63
|
+
|
|
64
|
+
return x_min, x_max, y_min, y_max, z_min, z_max
|
|
65
|
+
|
|
66
|
+
class ImageStats:
|
|
67
|
+
"""Class holding metadata about an image, optimized with Numba
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
image : numpy ndarray
|
|
72
|
+
the image who's metadata is being stored
|
|
73
|
+
|
|
74
|
+
Attributes
|
|
75
|
+
----------
|
|
76
|
+
min_intensity : float
|
|
77
|
+
the minimum intensity of a pixel/voxel in the given image
|
|
78
|
+
max_intensity : float
|
|
79
|
+
the maximum intensity of a pixel/voxel in the given image
|
|
80
|
+
x_min : int
|
|
81
|
+
the smallest x-coordinate of the given image
|
|
82
|
+
y_min : int
|
|
83
|
+
the smallest y-coordinate of the given image
|
|
84
|
+
z_min : int
|
|
85
|
+
the smallest z-coordinate of the given image
|
|
86
|
+
x_max : int
|
|
87
|
+
the largest x-coordinate of the given image
|
|
88
|
+
y_max : int
|
|
89
|
+
the largest y-coordinate of the given image
|
|
90
|
+
z_max : int
|
|
91
|
+
the largest z-coordinate of the given image
|
|
92
|
+
"""
|
|
93
|
+
# Use __slots__ for memory efficiency and faster attribute access
|
|
94
|
+
__slots__ = (
|
|
95
|
+
'_min_intensity', '_max_intensity',
|
|
96
|
+
'_x_min', '_y_min', '_z_min',
|
|
97
|
+
'_x_max', '_y_max', '_z_max'
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def __init__(self, image: np.ndarray):
|
|
101
|
+
# Input validation
|
|
102
|
+
if image is None:
|
|
103
|
+
raise TypeError("Image cannot be None")
|
|
104
|
+
if len(image) == 0:
|
|
105
|
+
raise ValueError("Image cannot be empty")
|
|
106
|
+
|
|
107
|
+
# Convert image to a numpy array if it isn't already
|
|
108
|
+
if not isinstance(image, np.ndarray):
|
|
109
|
+
image = np.asarray(image)
|
|
110
|
+
|
|
111
|
+
# Compute intensity range using Numba-optimized function
|
|
112
|
+
min_intensity, max_intensity = compute_image_intensity_range(image)
|
|
113
|
+
self._min_intensity = min_intensity
|
|
114
|
+
self._max_intensity = max_intensity
|
|
115
|
+
|
|
116
|
+
# Compute image dimensions and coordinate ranges
|
|
117
|
+
self._x_min, self._x_max, self._y_min, self._y_max, self._z_min, self._z_max = compute_image_dimensions(image.shape)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def min_intensity(self) -> float:
|
|
121
|
+
return self._min_intensity
|
|
122
|
+
|
|
123
|
+
@min_intensity.setter
|
|
124
|
+
def min_intensity(self, value: float):
|
|
125
|
+
if value is None:
|
|
126
|
+
raise TypeError("min_intensity cannot be None")
|
|
127
|
+
self._min_intensity = float(value)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def max_intensity(self) -> float:
|
|
131
|
+
return self._max_intensity
|
|
132
|
+
|
|
133
|
+
@max_intensity.setter
|
|
134
|
+
def max_intensity(self, value: float):
|
|
135
|
+
if value is None:
|
|
136
|
+
raise TypeError("max_intensity cannot be None")
|
|
137
|
+
self._max_intensity = float(value)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def x_min(self) -> float:
|
|
141
|
+
return self._x_min
|
|
142
|
+
|
|
143
|
+
@x_min.setter
|
|
144
|
+
def x_min(self, value: float):
|
|
145
|
+
if value is None:
|
|
146
|
+
raise TypeError("x_min cannot be None")
|
|
147
|
+
self._x_min = float(value)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def y_min(self) -> float:
|
|
151
|
+
return self._y_min
|
|
152
|
+
|
|
153
|
+
@y_min.setter
|
|
154
|
+
def y_min(self, value: float):
|
|
155
|
+
if value is None:
|
|
156
|
+
raise TypeError("y_min cannot be None")
|
|
157
|
+
self._y_min = float(value)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def z_min(self) -> float:
|
|
161
|
+
return self._z_min
|
|
162
|
+
|
|
163
|
+
@z_min.setter
|
|
164
|
+
def z_min(self, value: float):
|
|
165
|
+
if value is None:
|
|
166
|
+
raise TypeError("z_min cannot be None")
|
|
167
|
+
self._z_min = float(value)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def x_max(self) -> float:
|
|
171
|
+
return self._x_max
|
|
172
|
+
|
|
173
|
+
@x_max.setter
|
|
174
|
+
def x_max(self, value: float):
|
|
175
|
+
if value is None:
|
|
176
|
+
raise TypeError("x_max cannot be None")
|
|
177
|
+
self._x_max = float(value)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def y_max(self) -> float:
|
|
181
|
+
return self._y_max
|
|
182
|
+
|
|
183
|
+
@y_max.setter
|
|
184
|
+
def y_max(self, value: float):
|
|
185
|
+
if value is None:
|
|
186
|
+
raise TypeError("y_max cannot be None")
|
|
187
|
+
self._y_max = float(value)
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def z_max(self) -> float:
|
|
191
|
+
return self._z_max
|
|
192
|
+
|
|
193
|
+
@z_max.setter
|
|
194
|
+
def z_max(self, value: float):
|
|
195
|
+
if value is None:
|
|
196
|
+
raise TypeError("z_max cannot be None")
|
|
197
|
+
self._z_max = float(value)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .inputs import CostFunction, HeuristicFunction, SearchFunction
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Enums for the types of inputs for the cost, heuristic and search functions.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
class CostFunction(Enum):
|
|
7
|
+
RECIPROCAL = "reciprocal"
|
|
8
|
+
|
|
9
|
+
class HeuristicFunction(Enum):
|
|
10
|
+
EUCLIDEAN = "euclidean"
|
|
11
|
+
|
|
12
|
+
class SearchFunction(Enum):
|
|
13
|
+
ASTAR = "astar"
|
|
14
|
+
NBASTAR = "nbastar"
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numba as nb
|
|
3
|
+
|
|
4
|
+
# Numba-optimized helper functions
|
|
5
|
+
@nb.njit(fastmath=True)
|
|
6
|
+
def compute_f_score(g_score: float, h_score: float) -> float:
|
|
7
|
+
"""Compute f_score from g_score and h_score with Numba optimization"""
|
|
8
|
+
return g_score + h_score
|
|
9
|
+
|
|
10
|
+
@nb.njit
|
|
11
|
+
def validate_point(point):
|
|
12
|
+
"""Validate point array with Numba optimization"""
|
|
13
|
+
return len(point) > 0
|
|
14
|
+
|
|
15
|
+
@nb.njit
|
|
16
|
+
def get_score_by_direction(from_start, start_score, goal_score):
|
|
17
|
+
"""Get the appropriate score based on direction with Numba optimization"""
|
|
18
|
+
if from_start:
|
|
19
|
+
return start_score
|
|
20
|
+
return goal_score
|
|
21
|
+
|
|
22
|
+
class BidirectionalNode:
|
|
23
|
+
"""Class holding attributes and properties of a Bidirectional Node, optimized for performance
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
point : numpy ndarray
|
|
28
|
+
the 2D/3D coordinates of the node (can be a pixel or a voxel)
|
|
29
|
+
g_score_from_start : float
|
|
30
|
+
the distance from a starting node to the current node
|
|
31
|
+
g_score_from_goal : float
|
|
32
|
+
the distance from a goal node to the current node
|
|
33
|
+
h_score_from_start : float
|
|
34
|
+
the estimated distance from the current node to a goal node
|
|
35
|
+
h_score_from_goal : float
|
|
36
|
+
the estimated distance from the current node to a start node
|
|
37
|
+
predecessor_from_start : BidirectionalNode
|
|
38
|
+
the current node's immediate predecessor, from which we
|
|
39
|
+
travelled to the current node
|
|
40
|
+
The predecessor's first ancestor is the start node
|
|
41
|
+
predecessor_from_goal : BidirectionalNode
|
|
42
|
+
the current node's immediate predecessor, from which we
|
|
43
|
+
travelled to the current node
|
|
44
|
+
The predecessor's first ancestor is the goal node
|
|
45
|
+
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
point : numpy ndarray
|
|
49
|
+
the 2D/3D coordinates of the node (can be a pixel or a voxel)
|
|
50
|
+
g_score_from_start : float
|
|
51
|
+
the distance from a starting node to the current node
|
|
52
|
+
g_score_from_goal : float
|
|
53
|
+
the distance from a goal node to the current node
|
|
54
|
+
h_score_from_start : float
|
|
55
|
+
the estimated distance from the current node to a goal node
|
|
56
|
+
h_score_from_goal : float
|
|
57
|
+
the estimated distance from the current node to a start node
|
|
58
|
+
f_score_from_start : float
|
|
59
|
+
the sum of g_score_from_start and h_score_from_start
|
|
60
|
+
f_score_from_goal : float
|
|
61
|
+
the sum of g_score_from_goal and h_score_from_goal
|
|
62
|
+
predecessor_from_start : BidirectionalNode
|
|
63
|
+
the current node's immediate predecessor, from which we
|
|
64
|
+
travelled to the current node
|
|
65
|
+
The predecessor's first ancestor is the start node
|
|
66
|
+
predecessor_from_goal : BidirectionalNode
|
|
67
|
+
the current node's immediate predecessor, from which we
|
|
68
|
+
travelled to the current node The predecessor's first ancestor
|
|
69
|
+
is the goal node
|
|
70
|
+
"""
|
|
71
|
+
# Use __slots__ to reduce memory overhead and access time
|
|
72
|
+
__slots__ = (
|
|
73
|
+
'_point', '_g_score_from_start', '_g_score_from_goal',
|
|
74
|
+
'_h_score_from_start', '_h_score_from_goal',
|
|
75
|
+
'_f_score_from_start', '_f_score_from_goal',
|
|
76
|
+
'_predecessor_from_start', '_predecessor_from_goal'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
point: np.ndarray,
|
|
82
|
+
g_score_from_start: float = float('inf'),
|
|
83
|
+
g_score_from_goal: float = float('inf'),
|
|
84
|
+
h_score_from_start: float = float('inf'),
|
|
85
|
+
h_score_from_goal: float = float('inf'),
|
|
86
|
+
f_score_from_start: float = float('inf'),
|
|
87
|
+
f_score_from_goal: float = float('inf'),
|
|
88
|
+
predecessor_from_start: 'BidirectionalNode' = None,
|
|
89
|
+
predecessor_from_goal: 'BidirectionalNode' = None
|
|
90
|
+
):
|
|
91
|
+
# Convert point to int64 for better performance with Numba
|
|
92
|
+
if isinstance(point, list):
|
|
93
|
+
point = np.array(point)
|
|
94
|
+
if not isinstance(point, np.ndarray):
|
|
95
|
+
point = np.array(point)
|
|
96
|
+
if point.dtype != np.int64:
|
|
97
|
+
point = point.astype(np.int64)
|
|
98
|
+
|
|
99
|
+
self._point = point
|
|
100
|
+
self._g_score_from_start = float(g_score_from_start)
|
|
101
|
+
self._g_score_from_goal = float(g_score_from_goal)
|
|
102
|
+
self._h_score_from_start = float(h_score_from_start)
|
|
103
|
+
self._h_score_from_goal = float(h_score_from_goal)
|
|
104
|
+
|
|
105
|
+
# Use Numba functions for f_score calculations if they're not provided
|
|
106
|
+
if f_score_from_start == float('inf') and g_score_from_start != float('inf') and h_score_from_start != float('inf'):
|
|
107
|
+
self._f_score_from_start = compute_f_score(g_score_from_start, h_score_from_start)
|
|
108
|
+
else:
|
|
109
|
+
self._f_score_from_start = float(f_score_from_start)
|
|
110
|
+
|
|
111
|
+
if f_score_from_goal == float('inf') and g_score_from_goal != float('inf') and h_score_from_goal != float('inf'):
|
|
112
|
+
self._f_score_from_goal = compute_f_score(g_score_from_goal, h_score_from_goal)
|
|
113
|
+
else:
|
|
114
|
+
self._f_score_from_goal = float(f_score_from_goal)
|
|
115
|
+
|
|
116
|
+
self._predecessor_from_start = predecessor_from_start
|
|
117
|
+
self._predecessor_from_goal = predecessor_from_goal
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def point(self):
|
|
121
|
+
return self._point
|
|
122
|
+
|
|
123
|
+
@point.setter
|
|
124
|
+
def point(self, value: np.ndarray):
|
|
125
|
+
if value is None:
|
|
126
|
+
raise TypeError
|
|
127
|
+
if not validate_point(value):
|
|
128
|
+
raise ValueError
|
|
129
|
+
# Ensure int64 type for better performance
|
|
130
|
+
if not isinstance(value, np.ndarray):
|
|
131
|
+
value = np.array(value, dtype=np.int64)
|
|
132
|
+
if value.dtype != np.int64:
|
|
133
|
+
value = value.astype(np.int64)
|
|
134
|
+
self._point = value
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def g_score_from_start(self):
|
|
138
|
+
return self._g_score_from_start
|
|
139
|
+
|
|
140
|
+
@g_score_from_start.setter
|
|
141
|
+
def g_score_from_start(self, value: float):
|
|
142
|
+
self._g_score_from_start = float(value)
|
|
143
|
+
# Update f_score when g_score changes
|
|
144
|
+
if self._h_score_from_start != float('inf'):
|
|
145
|
+
self._f_score_from_start = compute_f_score(value, self._h_score_from_start)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def g_score_from_goal(self):
|
|
149
|
+
return self._g_score_from_goal
|
|
150
|
+
|
|
151
|
+
@g_score_from_goal.setter
|
|
152
|
+
def g_score_from_goal(self, value: float):
|
|
153
|
+
self._g_score_from_goal = float(value)
|
|
154
|
+
# Update f_score when g_score changes
|
|
155
|
+
if self._h_score_from_goal != float('inf'):
|
|
156
|
+
self._f_score_from_goal = compute_f_score(value, self._h_score_from_goal)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def h_score_from_start(self):
|
|
160
|
+
return self._h_score_from_start
|
|
161
|
+
|
|
162
|
+
@h_score_from_start.setter
|
|
163
|
+
def h_score_from_start(self, value: float):
|
|
164
|
+
self._h_score_from_start = float(value)
|
|
165
|
+
# Update f_score when h_score changes
|
|
166
|
+
if self._g_score_from_start != float('inf'):
|
|
167
|
+
self._f_score_from_start = compute_f_score(self._g_score_from_start, value)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def h_score_from_goal(self):
|
|
171
|
+
return self._h_score_from_goal
|
|
172
|
+
|
|
173
|
+
@h_score_from_goal.setter
|
|
174
|
+
def h_score_from_goal(self, value: float):
|
|
175
|
+
self._h_score_from_goal = float(value)
|
|
176
|
+
# Update f_score when h_score changes
|
|
177
|
+
if self._g_score_from_goal != float('inf'):
|
|
178
|
+
self._f_score_from_goal = compute_f_score(self._g_score_from_goal, value)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def f_score_from_start(self):
|
|
182
|
+
return self._f_score_from_start
|
|
183
|
+
|
|
184
|
+
@f_score_from_start.setter
|
|
185
|
+
def f_score_from_start(self, value: float):
|
|
186
|
+
self._f_score_from_start = float(value)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def f_score_from_goal(self):
|
|
190
|
+
return self._f_score_from_goal
|
|
191
|
+
|
|
192
|
+
@f_score_from_goal.setter
|
|
193
|
+
def f_score_from_goal(self, value: float):
|
|
194
|
+
self._f_score_from_goal = float(value)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def predecessor_from_start(self):
|
|
198
|
+
return self._predecessor_from_start
|
|
199
|
+
|
|
200
|
+
@predecessor_from_start.setter
|
|
201
|
+
def predecessor_from_start(self, value):
|
|
202
|
+
self._predecessor_from_start = value
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def predecessor_from_goal(self):
|
|
206
|
+
return self._predecessor_from_goal
|
|
207
|
+
|
|
208
|
+
@predecessor_from_goal.setter
|
|
209
|
+
def predecessor_from_goal(self, value):
|
|
210
|
+
self._predecessor_from_goal = value
|
|
211
|
+
|
|
212
|
+
# Fast accessor methods optimized for performance
|
|
213
|
+
def get_g(self, from_start: bool) -> float:
|
|
214
|
+
"""Get the appropriate g_score based on direction"""
|
|
215
|
+
return get_score_by_direction(from_start, self._g_score_from_start, self._g_score_from_goal)
|
|
216
|
+
|
|
217
|
+
def get_f(self, from_start: bool) -> float:
|
|
218
|
+
"""Get the appropriate f_score based on direction"""
|
|
219
|
+
return get_score_by_direction(from_start, self._f_score_from_start, self._f_score_from_goal)
|
|
220
|
+
|
|
221
|
+
def set_g(self, g_score: float, from_start: bool):
|
|
222
|
+
"""Set the appropriate g_score based on direction"""
|
|
223
|
+
if from_start:
|
|
224
|
+
self.g_score_from_start = g_score
|
|
225
|
+
else:
|
|
226
|
+
self.g_score_from_goal = g_score
|
|
227
|
+
|
|
228
|
+
def set_f(self, f_score: float, from_start: bool):
|
|
229
|
+
"""Set the appropriate f_score based on direction"""
|
|
230
|
+
if from_start:
|
|
231
|
+
self.f_score_from_start = f_score
|
|
232
|
+
else:
|
|
233
|
+
self.f_score_from_goal = f_score
|
|
234
|
+
|
|
235
|
+
def set_predecessor(self, predecessor, from_start: bool):
|
|
236
|
+
"""Set the appropriate predecessor based on direction"""
|
|
237
|
+
if from_start:
|
|
238
|
+
self.predecessor_from_start = predecessor
|
|
239
|
+
else:
|
|
240
|
+
self.predecessor_from_goal = predecessor
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numba as nb
|
|
3
|
+
|
|
4
|
+
# Numba-optimized helper functions for node operations
|
|
5
|
+
@nb.njit(fastmath=True)
|
|
6
|
+
def compute_f_score(g_score: float, h_score: float) -> float:
|
|
7
|
+
"""Compute f_score from g_score and h_score with Numba optimization"""
|
|
8
|
+
return g_score + h_score
|
|
9
|
+
|
|
10
|
+
@nb.njit
|
|
11
|
+
def validate_point(point):
|
|
12
|
+
"""Validate point array with Numba optimization"""
|
|
13
|
+
return len(point) > 0
|
|
14
|
+
|
|
15
|
+
class Node:
|
|
16
|
+
"""Class holding information about a node, optimized for numerical operations
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
point : numpy ndarray
|
|
21
|
+
the 2D/3D coordinates of the node (can be a pixel or a voxel)
|
|
22
|
+
g_score : float
|
|
23
|
+
the distance from a starting node to the current node
|
|
24
|
+
h_score : float
|
|
25
|
+
the estimated distance from the current node to a goal_node
|
|
26
|
+
predecessor : Node
|
|
27
|
+
the current node's immediate predecessor, from which we
|
|
28
|
+
travelled to the current node
|
|
29
|
+
|
|
30
|
+
Attributes
|
|
31
|
+
----------
|
|
32
|
+
point : numpy ndarray
|
|
33
|
+
the 2D/3D coordinates of the node
|
|
34
|
+
g_score : float
|
|
35
|
+
the actual cost from a starting node to the current node
|
|
36
|
+
h_score : float
|
|
37
|
+
the estimated cost from the current node to a goal_node
|
|
38
|
+
f_score : float
|
|
39
|
+
the sum of the node's g_score and h_score
|
|
40
|
+
predecessor : Node
|
|
41
|
+
the current node's immediate predecessor, from which we
|
|
42
|
+
travelled to the current node
|
|
43
|
+
"""
|
|
44
|
+
__slots__ = ('_point', '_g_score', '_h_score', '_f_score', '_predecessor')
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
point: np.ndarray,
|
|
49
|
+
g_score: float,
|
|
50
|
+
h_score: float,
|
|
51
|
+
predecessor: 'Node' = None
|
|
52
|
+
):
|
|
53
|
+
# Convert point to int64 for better performance with Numba
|
|
54
|
+
if isinstance(point, list):
|
|
55
|
+
point = np.array(point)
|
|
56
|
+
if not isinstance(point, np.ndarray):
|
|
57
|
+
point = np.array(point)
|
|
58
|
+
if point.dtype != np.int64:
|
|
59
|
+
point = point.astype(np.int64)
|
|
60
|
+
|
|
61
|
+
self._point = point
|
|
62
|
+
self._g_score = float(g_score)
|
|
63
|
+
self._h_score = float(h_score)
|
|
64
|
+
# Use Numba function for f_score calculation
|
|
65
|
+
self._f_score = compute_f_score(g_score, h_score)
|
|
66
|
+
self._predecessor = predecessor
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def point(self):
|
|
70
|
+
return self._point
|
|
71
|
+
|
|
72
|
+
@point.setter
|
|
73
|
+
def point(self, value: np.ndarray):
|
|
74
|
+
if value is None:
|
|
75
|
+
raise TypeError
|
|
76
|
+
if not validate_point(value):
|
|
77
|
+
raise ValueError
|
|
78
|
+
# Ensure int64 type for better performance
|
|
79
|
+
if not isinstance(value, np.ndarray):
|
|
80
|
+
value = np.array(value, dtype=np.int64)
|
|
81
|
+
if value.dtype != np.int64:
|
|
82
|
+
value = value.astype(np.int64)
|
|
83
|
+
self._point = value
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def g_score(self):
|
|
87
|
+
return self._g_score
|
|
88
|
+
|
|
89
|
+
@g_score.setter
|
|
90
|
+
def g_score(self, value: float):
|
|
91
|
+
if value is None:
|
|
92
|
+
raise TypeError
|
|
93
|
+
self._g_score = float(value)
|
|
94
|
+
# Update f_score when g_score changes
|
|
95
|
+
self._f_score = compute_f_score(self._g_score, self._h_score)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def h_score(self):
|
|
99
|
+
return self._h_score
|
|
100
|
+
|
|
101
|
+
@h_score.setter
|
|
102
|
+
def h_score(self, value: float):
|
|
103
|
+
if value is None:
|
|
104
|
+
raise TypeError
|
|
105
|
+
self._h_score = float(value)
|
|
106
|
+
# Update f_score when h_score changes
|
|
107
|
+
self._f_score = compute_f_score(self._g_score, self._h_score)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def f_score(self):
|
|
111
|
+
return self._f_score
|
|
112
|
+
|
|
113
|
+
@f_score.setter
|
|
114
|
+
def f_score(self, value: float):
|
|
115
|
+
if value is None:
|
|
116
|
+
raise TypeError
|
|
117
|
+
self._f_score = float(value)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def predecessor(self):
|
|
121
|
+
return self._predecessor
|
|
122
|
+
|
|
123
|
+
@predecessor.setter
|
|
124
|
+
def predecessor(self, value):
|
|
125
|
+
self._predecessor = value
|