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,574 @@
|
|
|
1
|
+
import napari
|
|
2
|
+
import numpy as np
|
|
3
|
+
from qtpy.QtWidgets import (
|
|
4
|
+
QWidget, QVBoxLayout, QPushButton, QLabel,
|
|
5
|
+
QHBoxLayout, QFrame, QListWidget, QListWidgetItem,
|
|
6
|
+
QFileDialog
|
|
7
|
+
)
|
|
8
|
+
from qtpy.QtCore import Signal
|
|
9
|
+
|
|
10
|
+
class PathVisualizationWidget(QWidget):
|
|
11
|
+
"""Widget for managing and visualizing multiple paths"""
|
|
12
|
+
|
|
13
|
+
# Define signals
|
|
14
|
+
path_selected = Signal(str) # path_id
|
|
15
|
+
path_deleted = Signal(str) # path_id
|
|
16
|
+
|
|
17
|
+
def __init__(self, viewer, image, state):
|
|
18
|
+
"""Initialize the path visualization widget.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
-----------
|
|
22
|
+
viewer : napari.Viewer
|
|
23
|
+
The napari viewer instance
|
|
24
|
+
image : numpy.ndarray
|
|
25
|
+
3D or higher-dimensional image data
|
|
26
|
+
state : dict
|
|
27
|
+
Shared state dictionary between modules
|
|
28
|
+
"""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.viewer = viewer
|
|
31
|
+
self.image = image
|
|
32
|
+
self.state = state
|
|
33
|
+
|
|
34
|
+
self.xy_spacing_nm = self.state.get('xy_spacing_nm', 94.0)
|
|
35
|
+
|
|
36
|
+
# Flag to prevent recursive event handling
|
|
37
|
+
self.handling_event = False
|
|
38
|
+
|
|
39
|
+
# Setup UI
|
|
40
|
+
self.setup_ui()
|
|
41
|
+
|
|
42
|
+
def update_pixel_spacing(self, new_spacing):
|
|
43
|
+
"""Update pixel spacing for visualization module"""
|
|
44
|
+
self.pixel_spacing_nm = new_spacing
|
|
45
|
+
print(f"Visualization: Updated pixel spacing to {new_spacing:.1f} nm/pixel")
|
|
46
|
+
# Visualization module typically doesn't need parameter updates
|
|
47
|
+
# but spacing info could be used for distance calculations in path analysis
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def setup_ui(self):
|
|
51
|
+
"""Create the UI panel with controls"""
|
|
52
|
+
layout = QVBoxLayout()
|
|
53
|
+
layout.setSpacing(2)
|
|
54
|
+
layout.setContentsMargins(2, 2, 2, 2)
|
|
55
|
+
self.setLayout(layout)
|
|
56
|
+
|
|
57
|
+
# Path list with instructions
|
|
58
|
+
layout.addWidget(QLabel("Saved Paths (select to view or manipulate):"))
|
|
59
|
+
self.path_list = QListWidget()
|
|
60
|
+
self.path_list.setFixedHeight(120)
|
|
61
|
+
self.path_list.setSelectionMode(QListWidget.ExtendedSelection)
|
|
62
|
+
self.path_list.itemSelectionChanged.connect(self.on_path_selection_changed)
|
|
63
|
+
layout.addWidget(self.path_list)
|
|
64
|
+
|
|
65
|
+
# Path management buttons
|
|
66
|
+
path_buttons_layout = QHBoxLayout()
|
|
67
|
+
path_buttons_layout.setSpacing(2)
|
|
68
|
+
path_buttons_layout.setContentsMargins(2, 2, 2, 2)
|
|
69
|
+
|
|
70
|
+
self.view_path_btn = QPushButton("View Selected Path")
|
|
71
|
+
self.view_path_btn.setFixedHeight(22)
|
|
72
|
+
self.view_path_btn.clicked.connect(self.view_selected_path)
|
|
73
|
+
self.view_path_btn.setEnabled(False)
|
|
74
|
+
path_buttons_layout.addWidget(self.view_path_btn)
|
|
75
|
+
|
|
76
|
+
self.delete_path_btn = QPushButton("Delete Selected Path(s)")
|
|
77
|
+
self.delete_path_btn.setFixedHeight(22)
|
|
78
|
+
self.delete_path_btn.clicked.connect(self.delete_selected_paths)
|
|
79
|
+
self.delete_path_btn.setEnabled(False)
|
|
80
|
+
path_buttons_layout.addWidget(self.delete_path_btn)
|
|
81
|
+
|
|
82
|
+
layout.addLayout(path_buttons_layout)
|
|
83
|
+
|
|
84
|
+
# Add separator
|
|
85
|
+
separator = QFrame()
|
|
86
|
+
separator.setFrameShape(QFrame.HLine)
|
|
87
|
+
separator.setFrameShadow(QFrame.Sunken)
|
|
88
|
+
layout.addWidget(separator)
|
|
89
|
+
|
|
90
|
+
# Path connection button
|
|
91
|
+
self.connect_paths_btn = QPushButton("Connect Selected Paths")
|
|
92
|
+
self.connect_paths_btn.setFixedHeight(22)
|
|
93
|
+
self.connect_paths_btn.setToolTip("Select exactly 2 paths to connect them")
|
|
94
|
+
self.connect_paths_btn.clicked.connect(self.connect_selected_paths)
|
|
95
|
+
self.connect_paths_btn.setEnabled(False)
|
|
96
|
+
layout.addWidget(self.connect_paths_btn)
|
|
97
|
+
|
|
98
|
+
# Path visibility options
|
|
99
|
+
visibility_layout = QHBoxLayout()
|
|
100
|
+
visibility_layout.setSpacing(2)
|
|
101
|
+
visibility_layout.setContentsMargins(2, 2, 2, 2)
|
|
102
|
+
|
|
103
|
+
self.show_all_btn = QPushButton("Show All Paths")
|
|
104
|
+
self.show_all_btn.setFixedHeight(22)
|
|
105
|
+
self.show_all_btn.clicked.connect(lambda: self.set_paths_visibility(True))
|
|
106
|
+
visibility_layout.addWidget(self.show_all_btn)
|
|
107
|
+
|
|
108
|
+
self.hide_all_btn = QPushButton("Hide All Paths")
|
|
109
|
+
self.hide_all_btn.setFixedHeight(22)
|
|
110
|
+
self.hide_all_btn.clicked.connect(lambda: self.set_paths_visibility(False))
|
|
111
|
+
visibility_layout.addWidget(self.hide_all_btn)
|
|
112
|
+
|
|
113
|
+
layout.addLayout(visibility_layout)
|
|
114
|
+
|
|
115
|
+
# Export button
|
|
116
|
+
self.export_all_btn = QPushButton("Export All Paths")
|
|
117
|
+
self.export_all_btn.setFixedHeight(22)
|
|
118
|
+
self.export_all_btn.clicked.connect(self.export_all_paths)
|
|
119
|
+
self.export_all_btn.setEnabled(False)
|
|
120
|
+
layout.addWidget(self.export_all_btn)
|
|
121
|
+
|
|
122
|
+
# Status message
|
|
123
|
+
self.status_label = QLabel("")
|
|
124
|
+
layout.addWidget(self.status_label)
|
|
125
|
+
|
|
126
|
+
def update_path_list(self):
|
|
127
|
+
"""Update the path list with current paths"""
|
|
128
|
+
if self.handling_event:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
self.handling_event = True
|
|
133
|
+
|
|
134
|
+
# Clear current list
|
|
135
|
+
self.path_list.clear()
|
|
136
|
+
|
|
137
|
+
# Add paths to list
|
|
138
|
+
for path_id, path_data in self.state['paths'].items():
|
|
139
|
+
item = QListWidgetItem(path_data['name'])
|
|
140
|
+
item.setData(100, path_id) # Store path ID as custom data
|
|
141
|
+
self.path_list.addItem(item)
|
|
142
|
+
|
|
143
|
+
# Enable/disable export button
|
|
144
|
+
self.export_all_btn.setEnabled(len(self.state['paths']) > 0)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
napari.utils.notifications.show_info(f"Error updating path list: {str(e)}")
|
|
147
|
+
self.status_label.setText(f"Error: {str(e)}")
|
|
148
|
+
finally:
|
|
149
|
+
self.handling_event = False
|
|
150
|
+
|
|
151
|
+
def on_path_selection_changed(self):
|
|
152
|
+
"""Handle when path selection changes in the list"""
|
|
153
|
+
# Prevent processing during updates
|
|
154
|
+
if self.handling_event:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
self.handling_event = True
|
|
159
|
+
|
|
160
|
+
selected_items = self.path_list.selectedItems()
|
|
161
|
+
num_selected = len(selected_items)
|
|
162
|
+
|
|
163
|
+
# Enable/disable buttons based on selection
|
|
164
|
+
self.delete_path_btn.setEnabled(num_selected > 0)
|
|
165
|
+
self.view_path_btn.setEnabled(num_selected == 1)
|
|
166
|
+
self.connect_paths_btn.setEnabled(num_selected == 2)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
napari.utils.notifications.show_info(f"Error handling selection change: {str(e)}")
|
|
169
|
+
finally:
|
|
170
|
+
self.handling_event = False
|
|
171
|
+
|
|
172
|
+
def view_selected_path(self):
|
|
173
|
+
"""View the selected path from the list"""
|
|
174
|
+
if self.handling_event:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
self.handling_event = True
|
|
179
|
+
|
|
180
|
+
selected_items = self.path_list.selectedItems()
|
|
181
|
+
if len(selected_items) != 1:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
item = selected_items[0]
|
|
185
|
+
path_id = item.data(100)
|
|
186
|
+
|
|
187
|
+
# Emit signal that path is selected
|
|
188
|
+
self.path_selected.emit(path_id)
|
|
189
|
+
|
|
190
|
+
# Ensure the selected path's layer is visible
|
|
191
|
+
if path_id in self.state['path_layers']:
|
|
192
|
+
self.state['path_layers'][path_id].visible = True
|
|
193
|
+
|
|
194
|
+
napari.utils.notifications.show_info(f"Viewing {self.state['paths'][path_id]['name']}")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
napari.utils.notifications.show_info(f"Error viewing path: {str(e)}")
|
|
197
|
+
self.status_label.setText(f"Error: {str(e)}")
|
|
198
|
+
finally:
|
|
199
|
+
self.handling_event = False
|
|
200
|
+
|
|
201
|
+
def delete_selected_paths(self):
|
|
202
|
+
"""Delete the currently selected paths"""
|
|
203
|
+
selected_items = self.path_list.selectedItems()
|
|
204
|
+
if not selected_items:
|
|
205
|
+
napari.utils.notifications.show_info("No paths selected")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
paths_deleted = []
|
|
209
|
+
|
|
210
|
+
for item in selected_items:
|
|
211
|
+
# Get the path ID
|
|
212
|
+
path_id = item.data(100)
|
|
213
|
+
|
|
214
|
+
if path_id in self.state['paths']:
|
|
215
|
+
path_name = self.state['paths'][path_id]['name']
|
|
216
|
+
|
|
217
|
+
# Remove the path layer from viewer
|
|
218
|
+
if path_id in self.state['path_layers']:
|
|
219
|
+
self.viewer.layers.remove(self.state['path_layers'][path_id])
|
|
220
|
+
del self.state['path_layers'][path_id]
|
|
221
|
+
|
|
222
|
+
# Remove corresponding segmentation layer if it exists
|
|
223
|
+
seg_layer_name = f"Segmentation - {path_name}"
|
|
224
|
+
for layer in list(self.viewer.layers): # Create a copy of the list to safely modify during iteration
|
|
225
|
+
if layer.name == seg_layer_name:
|
|
226
|
+
self.viewer.layers.remove(layer)
|
|
227
|
+
if (
|
|
228
|
+
'segmentation_layer' in self.state and
|
|
229
|
+
self.state['segmentation_layer'] is not None and
|
|
230
|
+
self.state['segmentation_layer'].name == seg_layer_name
|
|
231
|
+
):
|
|
232
|
+
self.state['segmentation_layer'] = None
|
|
233
|
+
napari.utils.notifications.show_info(f"Removed segmentation layer for {path_name}")
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Remove corresponding spine layer if it exists
|
|
237
|
+
spine_layer_name = f"Spines - {path_name}"
|
|
238
|
+
for layer in list(self.viewer.layers):
|
|
239
|
+
if layer.name == spine_layer_name:
|
|
240
|
+
self.viewer.layers.remove(layer)
|
|
241
|
+
if path_id in self.state['spine_layers']:
|
|
242
|
+
del self.state['spine_layers'][path_id]
|
|
243
|
+
napari.utils.notifications.show_info(f"Removed spine layer for {path_name}")
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
# Remove from dictionary
|
|
247
|
+
del self.state['paths'][path_id]
|
|
248
|
+
|
|
249
|
+
# Track deleted path
|
|
250
|
+
paths_deleted.append(path_id)
|
|
251
|
+
|
|
252
|
+
napari.utils.notifications.show_info(f"Deleted {path_name}")
|
|
253
|
+
|
|
254
|
+
# Update traced path visualization if any paths were deleted
|
|
255
|
+
if paths_deleted and self.image.ndim > 2 and self.state['traced_path_layer'] is not None:
|
|
256
|
+
self._update_traced_path_visualization()
|
|
257
|
+
|
|
258
|
+
# Update path list
|
|
259
|
+
self.update_path_list()
|
|
260
|
+
|
|
261
|
+
# Emit signal for each deleted path
|
|
262
|
+
for path_id in paths_deleted:
|
|
263
|
+
self.path_deleted.emit(path_id)
|
|
264
|
+
|
|
265
|
+
def _update_traced_path_visualization(self):
|
|
266
|
+
"""Update the traced path visualization to reflect current paths"""
|
|
267
|
+
if 'traced_path_layer' not in self.state or self.state['traced_path_layer'] is None:
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
if not self.state['paths']:
|
|
271
|
+
# If no paths remain, clear the traced path layer
|
|
272
|
+
self.state['traced_path_layer'].data = np.empty((0, self.image.ndim))
|
|
273
|
+
self.state['traced_path_layer'].visible = False
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
# Create a comprehensive visualization of all paths in the traced layer
|
|
277
|
+
all_traced_points = []
|
|
278
|
+
|
|
279
|
+
# First, determine the full z-range for all paths
|
|
280
|
+
min_z = float('inf')
|
|
281
|
+
max_z = float('-inf')
|
|
282
|
+
|
|
283
|
+
for path_id, path_data in self.state['paths'].items():
|
|
284
|
+
if len(path_data['data']) > 0 and path_data['visible']:
|
|
285
|
+
z_values = [point[0] for point in path_data['data']]
|
|
286
|
+
path_min_z = int(min(z_values))
|
|
287
|
+
path_max_z = int(max(z_values))
|
|
288
|
+
|
|
289
|
+
min_z = min(min_z, path_min_z)
|
|
290
|
+
max_z = max(max_z, path_max_z)
|
|
291
|
+
|
|
292
|
+
# If we have valid z-range
|
|
293
|
+
if min_z != float('inf') and max_z != float('-inf'):
|
|
294
|
+
# For each frame in the full range
|
|
295
|
+
for z in range(min_z, max_z + 1):
|
|
296
|
+
# Add all paths to this frame
|
|
297
|
+
for path_id, path_data in self.state['paths'].items():
|
|
298
|
+
if path_data['visible']:
|
|
299
|
+
for point in path_data['data']:
|
|
300
|
+
# Create a new point with the current frame's z-coordinate
|
|
301
|
+
new_point = point.copy()
|
|
302
|
+
new_point[0] = z # Set the z-coordinate to the current frame
|
|
303
|
+
all_traced_points.append(new_point)
|
|
304
|
+
|
|
305
|
+
# Update the traced path layer
|
|
306
|
+
if all_traced_points:
|
|
307
|
+
self.state['traced_path_layer'].data = np.array(all_traced_points)
|
|
308
|
+
self.state['traced_path_layer'].visible = True
|
|
309
|
+
else:
|
|
310
|
+
self.state['traced_path_layer'].data = np.empty((0, self.image.ndim))
|
|
311
|
+
self.state['traced_path_layer'].visible = False
|
|
312
|
+
|
|
313
|
+
def set_paths_visibility(self, visible):
|
|
314
|
+
"""Set visibility of all saved path layers and update traced path visualization"""
|
|
315
|
+
if self.handling_event:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
self.handling_event = True
|
|
320
|
+
|
|
321
|
+
# Show/hide individual path layers
|
|
322
|
+
for path_id, layer in self.state['path_layers'].items():
|
|
323
|
+
layer.visible = visible
|
|
324
|
+
self.state['paths'][path_id]['visible'] = visible
|
|
325
|
+
|
|
326
|
+
# Update traced path visualization only if we have a traced path layer
|
|
327
|
+
if self.image.ndim > 2 and self.state['traced_path_layer'] is not None:
|
|
328
|
+
if visible:
|
|
329
|
+
self._update_traced_path_visualization()
|
|
330
|
+
else:
|
|
331
|
+
# Hide traced path layer when hiding all paths
|
|
332
|
+
self.state['traced_path_layer'].data = np.empty((0, self.image.ndim))
|
|
333
|
+
self.state['traced_path_layer'].visible = False
|
|
334
|
+
|
|
335
|
+
action = "shown" if visible else "hidden"
|
|
336
|
+
napari.utils.notifications.show_info(f"All paths {action}")
|
|
337
|
+
except Exception as e:
|
|
338
|
+
napari.utils.notifications.show_info(f"Error updating path visibility: {str(e)}")
|
|
339
|
+
self.status_label.setText(f"Error: {str(e)}")
|
|
340
|
+
finally:
|
|
341
|
+
self.handling_event = False
|
|
342
|
+
|
|
343
|
+
def connect_selected_paths(self):
|
|
344
|
+
"""Connect two selected paths"""
|
|
345
|
+
if self.handling_event:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
self.handling_event = True
|
|
350
|
+
|
|
351
|
+
selected_items = self.path_list.selectedItems()
|
|
352
|
+
|
|
353
|
+
if len(selected_items) != 2:
|
|
354
|
+
napari.utils.notifications.show_info("Please select exactly two paths to connect")
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
# Get the path IDs
|
|
358
|
+
path_id1 = selected_items[0].data(100)
|
|
359
|
+
path_id2 = selected_items[1].data(100)
|
|
360
|
+
|
|
361
|
+
if path_id1 not in self.state['paths'] or path_id2 not in self.state['paths']:
|
|
362
|
+
napari.utils.notifications.show_info("Invalid path selection")
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Get the path data
|
|
366
|
+
path1 = self.state['paths'][path_id1]
|
|
367
|
+
path2 = self.state['paths'][path_id2]
|
|
368
|
+
|
|
369
|
+
# Check if paths have start/end points
|
|
370
|
+
if path1['start'] is None or path2['end'] is None:
|
|
371
|
+
napari.utils.notifications.show_info("Both paths must have start and end points to connect them")
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
# Get start of path1 and end of path2
|
|
375
|
+
start_point = path1['start']
|
|
376
|
+
end_point = path2['end']
|
|
377
|
+
|
|
378
|
+
napari.utils.notifications.show_info(f"Connecting {path1['name']} to {path2['name']}...")
|
|
379
|
+
|
|
380
|
+
# Determine if we're doing 2D or 3D search
|
|
381
|
+
is_same_frame = True
|
|
382
|
+
if self.image.ndim > 2:
|
|
383
|
+
is_same_frame = start_point[0] == end_point[0]
|
|
384
|
+
|
|
385
|
+
# Import necessary classes
|
|
386
|
+
import sys
|
|
387
|
+
sys.path.append('../path_tracing/brightest-path-lib')
|
|
388
|
+
from neuro_sam.brightest_path_lib.algorithm import BidirectionalAStarSearch
|
|
389
|
+
|
|
390
|
+
# Prepare points format based on 2D or 3D
|
|
391
|
+
if is_same_frame and self.image.ndim > 2:
|
|
392
|
+
# 2D case: use [y, x] format (ignore z)
|
|
393
|
+
search_start = start_point[1:3] # [y, x]
|
|
394
|
+
search_end = end_point[1:3] # [y, x]
|
|
395
|
+
search_image = self.image[int(start_point[0])]
|
|
396
|
+
napari.utils.notifications.show_info(f"Using 2D path search on frame {int(start_point[0])}")
|
|
397
|
+
else:
|
|
398
|
+
# 3D case or already 2D image: use full coordinates
|
|
399
|
+
search_start = start_point
|
|
400
|
+
search_end = end_point
|
|
401
|
+
search_image = self.image
|
|
402
|
+
napari.utils.notifications.show_info("Using 3D path search across frames")
|
|
403
|
+
|
|
404
|
+
# Search for connecting path
|
|
405
|
+
search_algorithm = BidirectionalAStarSearch(
|
|
406
|
+
search_image,
|
|
407
|
+
start_point=search_start,
|
|
408
|
+
goal_point=search_end
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
connecting_path = search_algorithm.search()
|
|
412
|
+
|
|
413
|
+
# If path found, create combined path
|
|
414
|
+
if connecting_path is not None and len(connecting_path) > 0:
|
|
415
|
+
# Fix coordinates if needed (2D case)
|
|
416
|
+
if is_same_frame and self.image.ndim > 2:
|
|
417
|
+
z_val = start_point[0]
|
|
418
|
+
fixed_connecting_path = []
|
|
419
|
+
for point in connecting_path:
|
|
420
|
+
if len(point) == 2: # [y, x]
|
|
421
|
+
fixed_connecting_path.append([z_val, point[0], point[1]])
|
|
422
|
+
else:
|
|
423
|
+
fixed_connecting_path.append(point)
|
|
424
|
+
connecting_path = fixed_connecting_path
|
|
425
|
+
|
|
426
|
+
# Convert to numpy arrays
|
|
427
|
+
path1_data = path1['data']
|
|
428
|
+
path2_data = path2['data']
|
|
429
|
+
connecting_data = np.array(connecting_path)
|
|
430
|
+
|
|
431
|
+
# Create combined path
|
|
432
|
+
combined_path = np.vstack([path1_data, connecting_data, path2_data])
|
|
433
|
+
|
|
434
|
+
# Create a name for the combined path
|
|
435
|
+
combined_name = f"{path1['name']} + {path2['name']}"
|
|
436
|
+
|
|
437
|
+
# Get a color
|
|
438
|
+
colors = ['cyan', 'magenta', 'green', 'blue', 'orange',
|
|
439
|
+
'purple', 'teal', 'coral', 'gold', 'lavender']
|
|
440
|
+
color_idx = len(self.state['paths']) % len(colors)
|
|
441
|
+
combined_color = colors[color_idx]
|
|
442
|
+
|
|
443
|
+
# Create a new layer
|
|
444
|
+
combined_layer = self.viewer.add_points(
|
|
445
|
+
combined_path,
|
|
446
|
+
name=combined_name,
|
|
447
|
+
size=3,
|
|
448
|
+
face_color=combined_color,
|
|
449
|
+
opacity=0.7
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Generate a unique ID for this path
|
|
453
|
+
import uuid
|
|
454
|
+
path_id = str(uuid.uuid4())
|
|
455
|
+
|
|
456
|
+
# Combine waypoints from both paths
|
|
457
|
+
combined_waypoints = []
|
|
458
|
+
if 'waypoints' in path1 and path1['waypoints']:
|
|
459
|
+
combined_waypoints.extend(path1['waypoints'])
|
|
460
|
+
if 'waypoints' in path2 and path2['waypoints']:
|
|
461
|
+
combined_waypoints.extend(path2['waypoints'])
|
|
462
|
+
|
|
463
|
+
# Store the combined path
|
|
464
|
+
self.state['paths'][path_id] = {
|
|
465
|
+
'name': combined_name,
|
|
466
|
+
'data': combined_path,
|
|
467
|
+
'start': path1['start'].copy(),
|
|
468
|
+
'end': path2['end'].copy(),
|
|
469
|
+
'waypoints': combined_waypoints,
|
|
470
|
+
'visible': True,
|
|
471
|
+
'layer': combined_layer,
|
|
472
|
+
'original_clicks': [], # Empty since this is a generated path
|
|
473
|
+
'smoothed': False # Combined paths are not smoothed
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# Store reference to the layer
|
|
477
|
+
self.state['path_layers'][path_id] = combined_layer
|
|
478
|
+
|
|
479
|
+
# Update the path list
|
|
480
|
+
self.update_path_list()
|
|
481
|
+
|
|
482
|
+
# Update traced path visualization
|
|
483
|
+
if self.image.ndim > 2 and self.state['traced_path_layer'] is not None:
|
|
484
|
+
self._update_traced_path_visualization()
|
|
485
|
+
|
|
486
|
+
# Set the new combined path as the current path
|
|
487
|
+
self.state['current_path_id'] = path_id
|
|
488
|
+
|
|
489
|
+
# Select the new path in the list
|
|
490
|
+
for i in range(self.path_list.count()):
|
|
491
|
+
item = self.path_list.item(i)
|
|
492
|
+
if item.data(100) == path_id:
|
|
493
|
+
self.path_list.setCurrentItem(item)
|
|
494
|
+
break
|
|
495
|
+
|
|
496
|
+
# Update UI
|
|
497
|
+
msg = f"Connected {path1['name']} to {path2['name']} successfully"
|
|
498
|
+
napari.utils.notifications.show_info(msg)
|
|
499
|
+
self.status_label.setText(msg)
|
|
500
|
+
|
|
501
|
+
# Import the signal from the path tracing widget to properly notify other modules
|
|
502
|
+
from qtpy.QtCore import QTimer
|
|
503
|
+
|
|
504
|
+
# Emit path_created signal to update other modules
|
|
505
|
+
def emit_delayed_signal():
|
|
506
|
+
# Find the path tracing widget and emit its signal
|
|
507
|
+
parent_widget = self.parent()
|
|
508
|
+
while parent_widget:
|
|
509
|
+
if hasattr(parent_widget, 'path_tracing_widget'):
|
|
510
|
+
parent_widget.path_tracing_widget.path_created.emit(path_id, combined_name, combined_path)
|
|
511
|
+
break
|
|
512
|
+
parent_widget = parent_widget.parent()
|
|
513
|
+
|
|
514
|
+
# Also emit our own signal
|
|
515
|
+
self.path_selected.emit(path_id)
|
|
516
|
+
|
|
517
|
+
# Use a timer to emit the signal after the UI has updated
|
|
518
|
+
QTimer.singleShot(100, emit_delayed_signal)
|
|
519
|
+
else:
|
|
520
|
+
error_msg = f"Could not find a path connecting {path1['name']} to {path2['name']}"
|
|
521
|
+
napari.utils.notifications.show_info(error_msg)
|
|
522
|
+
self.status_label.setText(error_msg)
|
|
523
|
+
except Exception as e:
|
|
524
|
+
error_msg = f"Error connecting paths: {str(e)}"
|
|
525
|
+
napari.utils.notifications.show_info(error_msg)
|
|
526
|
+
self.status_label.setText(error_msg)
|
|
527
|
+
print(f"Error details: {str(e)}")
|
|
528
|
+
finally:
|
|
529
|
+
self.handling_event = False
|
|
530
|
+
|
|
531
|
+
def export_all_paths(self):
|
|
532
|
+
"""Export all paths to a file"""
|
|
533
|
+
if not self.state['paths']:
|
|
534
|
+
napari.utils.notifications.show_info("No paths to export")
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
# Get path to save file
|
|
538
|
+
filepath, _ = QFileDialog.getSaveFileName(
|
|
539
|
+
self, "Save All Paths", "", "NumPy Files (*.npz)"
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if not filepath:
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
# Prepare data for export
|
|
547
|
+
path_data = {}
|
|
548
|
+
for path_id, path_info in self.state['paths'].items():
|
|
549
|
+
export_data = {
|
|
550
|
+
'points': path_info['data'],
|
|
551
|
+
'start': path_info['start'] if 'start' in path_info and path_info['start'] is not None else np.array([]),
|
|
552
|
+
'end': path_info['end'] if 'end' in path_info and path_info['end'] is not None else np.array([]),
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# Include waypoints if available
|
|
556
|
+
if 'waypoints' in path_info and path_info['waypoints']:
|
|
557
|
+
export_data['waypoints'] = np.array(path_info['waypoints'])
|
|
558
|
+
|
|
559
|
+
path_data[path_info['name']] = export_data
|
|
560
|
+
|
|
561
|
+
# Save as NumPy archive
|
|
562
|
+
np.savez(filepath, paths=path_data)
|
|
563
|
+
|
|
564
|
+
napari.utils.notifications.show_info(f"All paths saved to {filepath}")
|
|
565
|
+
self.status_label.setText(f"Paths exported to {filepath}")
|
|
566
|
+
|
|
567
|
+
except Exception as e:
|
|
568
|
+
napari.utils.notifications.show_info(f"Error saving paths: {e}")
|
|
569
|
+
self.status_label.setText(f"Error: {str(e)}")
|
|
570
|
+
|
|
571
|
+
def update_path_visualization(self):
|
|
572
|
+
"""Update the visualization of paths after modifications"""
|
|
573
|
+
if self.state['traced_path_layer'] is not None:
|
|
574
|
+
self._update_traced_path_visualization()
|