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.
Files changed (93) hide show
  1. neuro_sam/__init__.py +1 -0
  2. neuro_sam/brightest_path_lib/__init__.py +5 -0
  3. neuro_sam/brightest_path_lib/algorithm/__init__.py +3 -0
  4. neuro_sam/brightest_path_lib/algorithm/astar.py +586 -0
  5. neuro_sam/brightest_path_lib/algorithm/waypointastar.py +449 -0
  6. neuro_sam/brightest_path_lib/algorithm/waypointastar_speedup.py +1007 -0
  7. neuro_sam/brightest_path_lib/connected_componen.py +329 -0
  8. neuro_sam/brightest_path_lib/cost/__init__.py +8 -0
  9. neuro_sam/brightest_path_lib/cost/cost.py +33 -0
  10. neuro_sam/brightest_path_lib/cost/reciprocal.py +90 -0
  11. neuro_sam/brightest_path_lib/cost/reciprocal_transonic.py +86 -0
  12. neuro_sam/brightest_path_lib/heuristic/__init__.py +2 -0
  13. neuro_sam/brightest_path_lib/heuristic/euclidean.py +101 -0
  14. neuro_sam/brightest_path_lib/heuristic/heuristic.py +29 -0
  15. neuro_sam/brightest_path_lib/image/__init__.py +1 -0
  16. neuro_sam/brightest_path_lib/image/stats.py +197 -0
  17. neuro_sam/brightest_path_lib/input/__init__.py +1 -0
  18. neuro_sam/brightest_path_lib/input/inputs.py +14 -0
  19. neuro_sam/brightest_path_lib/node/__init__.py +2 -0
  20. neuro_sam/brightest_path_lib/node/bidirectional_node.py +240 -0
  21. neuro_sam/brightest_path_lib/node/node.py +125 -0
  22. neuro_sam/brightest_path_lib/visualization/__init__.py +4 -0
  23. neuro_sam/brightest_path_lib/visualization/flythrough.py +133 -0
  24. neuro_sam/brightest_path_lib/visualization/flythrough_all.py +394 -0
  25. neuro_sam/brightest_path_lib/visualization/tube_data.py +385 -0
  26. neuro_sam/brightest_path_lib/visualization/tube_flythrough.py +227 -0
  27. neuro_sam/napari_utils/anisotropic_scaling.py +503 -0
  28. neuro_sam/napari_utils/color_utils.py +135 -0
  29. neuro_sam/napari_utils/contrasting_color_system.py +169 -0
  30. neuro_sam/napari_utils/main_widget.py +1016 -0
  31. neuro_sam/napari_utils/path_tracing_module.py +1016 -0
  32. neuro_sam/napari_utils/punet_widget.py +424 -0
  33. neuro_sam/napari_utils/segmentation_model.py +769 -0
  34. neuro_sam/napari_utils/segmentation_module.py +649 -0
  35. neuro_sam/napari_utils/visualization_module.py +574 -0
  36. neuro_sam/plugin.py +260 -0
  37. neuro_sam/punet/__init__.py +0 -0
  38. neuro_sam/punet/deepd3_model.py +231 -0
  39. neuro_sam/punet/prob_unet_deepd3.py +431 -0
  40. neuro_sam/punet/prob_unet_with_tversky.py +375 -0
  41. neuro_sam/punet/punet_inference.py +236 -0
  42. neuro_sam/punet/run_inference.py +145 -0
  43. neuro_sam/punet/unet_blocks.py +81 -0
  44. neuro_sam/punet/utils.py +52 -0
  45. neuro_sam-0.1.0.dist-info/METADATA +269 -0
  46. neuro_sam-0.1.0.dist-info/RECORD +93 -0
  47. neuro_sam-0.1.0.dist-info/WHEEL +5 -0
  48. neuro_sam-0.1.0.dist-info/entry_points.txt +2 -0
  49. neuro_sam-0.1.0.dist-info/licenses/LICENSE +21 -0
  50. neuro_sam-0.1.0.dist-info/top_level.txt +2 -0
  51. sam2/__init__.py +11 -0
  52. sam2/automatic_mask_generator.py +454 -0
  53. sam2/benchmark.py +92 -0
  54. sam2/build_sam.py +174 -0
  55. sam2/configs/sam2/sam2_hiera_b+.yaml +113 -0
  56. sam2/configs/sam2/sam2_hiera_l.yaml +117 -0
  57. sam2/configs/sam2/sam2_hiera_s.yaml +116 -0
  58. sam2/configs/sam2/sam2_hiera_t.yaml +118 -0
  59. sam2/configs/sam2.1/sam2.1_hiera_b+.yaml +116 -0
  60. sam2/configs/sam2.1/sam2.1_hiera_l.yaml +120 -0
  61. sam2/configs/sam2.1/sam2.1_hiera_s.yaml +119 -0
  62. sam2/configs/sam2.1/sam2.1_hiera_t.yaml +121 -0
  63. sam2/configs/sam2.1_training/sam2.1_hiera_b+_MOSE_finetune.yaml +339 -0
  64. sam2/configs/train.yaml +335 -0
  65. sam2/modeling/__init__.py +5 -0
  66. sam2/modeling/backbones/__init__.py +5 -0
  67. sam2/modeling/backbones/hieradet.py +317 -0
  68. sam2/modeling/backbones/image_encoder.py +134 -0
  69. sam2/modeling/backbones/utils.py +93 -0
  70. sam2/modeling/memory_attention.py +169 -0
  71. sam2/modeling/memory_encoder.py +181 -0
  72. sam2/modeling/position_encoding.py +239 -0
  73. sam2/modeling/sam/__init__.py +5 -0
  74. sam2/modeling/sam/mask_decoder.py +295 -0
  75. sam2/modeling/sam/prompt_encoder.py +202 -0
  76. sam2/modeling/sam/transformer.py +311 -0
  77. sam2/modeling/sam2_base.py +911 -0
  78. sam2/modeling/sam2_utils.py +323 -0
  79. sam2/sam2.1_hiera_b+.yaml +116 -0
  80. sam2/sam2.1_hiera_l.yaml +120 -0
  81. sam2/sam2.1_hiera_s.yaml +119 -0
  82. sam2/sam2.1_hiera_t.yaml +121 -0
  83. sam2/sam2_hiera_b+.yaml +113 -0
  84. sam2/sam2_hiera_l.yaml +117 -0
  85. sam2/sam2_hiera_s.yaml +116 -0
  86. sam2/sam2_hiera_t.yaml +118 -0
  87. sam2/sam2_image_predictor.py +475 -0
  88. sam2/sam2_video_predictor.py +1222 -0
  89. sam2/sam2_video_predictor_legacy.py +1172 -0
  90. sam2/utils/__init__.py +5 -0
  91. sam2/utils/amg.py +348 -0
  92. sam2/utils/misc.py +349 -0
  93. sam2/utils/transforms.py +118 -0
@@ -0,0 +1,1016 @@
1
+ import napari
2
+ import numpy as np
3
+ import uuid
4
+ from qtpy.QtWidgets import (
5
+ QWidget, QVBoxLayout, QPushButton, QLabel,
6
+ QHBoxLayout, QFrame, QCheckBox, QComboBox, QDoubleSpinBox
7
+ )
8
+ from qtpy.QtCore import Signal
9
+ # import sys
10
+ # sys.path.append('./brightest-path-lib/')
11
+ from neuro_sam.brightest_path_lib.algorithm.waypointastar_speedup import quick_accurate_optimized_search
12
+ from scipy.interpolate import splprep, splev
13
+
14
+
15
+ class PathSmoother:
16
+ """B-spline based path smoothing for dendrite traces with scaling support"""
17
+
18
+ def __init__(self):
19
+ pass
20
+
21
+ def smooth_path(self, path_points, spacing_xyz=(1.0, 1.0, 1.0), smoothing_factor=None,
22
+ num_points=None, preserve_endpoints=True):
23
+ """
24
+ Smooth a 3D path using B-spline interpolation
25
+
26
+ Args:
27
+ path_points: numpy array of shape (N, 3) with [z, y, x] coordinates
28
+ spacing_xyz: voxel spacing in (x, y, z) format for proper distance calculation
29
+ smoothing_factor: B-spline smoothing parameter (higher = more smooth)
30
+ num_points: number of points in smoothed path (None = same as input)
31
+ preserve_endpoints: whether to keep original start/end points
32
+
33
+ Returns:
34
+ Smoothed path as numpy array
35
+ """
36
+ if len(path_points) < 3:
37
+ return path_points.copy()
38
+
39
+ # Store original endpoints
40
+ start_point = path_points[0].copy()
41
+ end_point = path_points[-1].copy()
42
+
43
+ # Apply B-spline smoothing
44
+ smoothed_path = self._bspline_smooth_anisotropic(
45
+ path_points, spacing_xyz, smoothing_factor, num_points
46
+ )
47
+
48
+ # Restore endpoints if requested
49
+ if preserve_endpoints and len(smoothed_path) > 0:
50
+ smoothed_path[0] = start_point
51
+ smoothed_path[-1] = end_point
52
+
53
+ return smoothed_path
54
+
55
+ def _bspline_smooth_anisotropic(self, path_points, spacing_xyz, smoothing_factor=None, num_points=None):
56
+ """B-spline smoothingn"""
57
+ if smoothing_factor is None:
58
+ # Auto-determine smoothing factor based on path length
59
+ path_length = len(path_points)
60
+ anisotropy_factor = max(spacing_xyz) / min(spacing_xyz)
61
+ smoothing_factor = max(0, path_length - np.sqrt(2 * path_length)) * anisotropy_factor
62
+
63
+ if num_points is None:
64
+ num_points = len(path_points)
65
+
66
+ try:
67
+ # Scale coordinates by voxel spacing for proper distance calculation
68
+ # path_points are in [z, y, x] format, spacing is in (x, y, z) format
69
+ scaled_points = path_points.copy().astype(float)
70
+ scaled_points[:, 0] *= spacing_xyz[2] # Z scaling
71
+ scaled_points[:, 1] *= spacing_xyz[1] # Y scaling
72
+ scaled_points[:, 2] *= spacing_xyz[0] # X scaling
73
+
74
+ # Simplify path to remove grid jitter/staircasing which causes spline overshoots
75
+ # We only keep points that are a certain physical distance apart
76
+ simplified_points = [scaled_points[0]]
77
+ last_kept_idx = 0
78
+ min_dist_sq = 2.0 * 2.0 * min(spacing_xyz)**2 # roughly 2 pixels distance squared
79
+
80
+ for i in range(1, len(scaled_points) - 1):
81
+ # Calculate distance to last kept point
82
+ diff = scaled_points[i] - scaled_points[last_kept_idx]
83
+ dist_sq = np.sum(diff**2)
84
+
85
+ if dist_sq > min_dist_sq:
86
+ simplified_points.append(scaled_points[i])
87
+ last_kept_idx = i
88
+
89
+ # Handling the final point
90
+ # If the last kept point is too close to the end point, we replace it with the end point
91
+ # This prevents "dancing" or jitter at the end where two points are very close
92
+ last_added = simplified_points[-1]
93
+ end_point = scaled_points[-1]
94
+ diff = end_point - last_added
95
+ dist_sq = np.sum(diff**2)
96
+
97
+ if dist_sq < min_dist_sq and len(simplified_points) > 1:
98
+ # Replace the last added intermediate point with the true end point
99
+ simplified_points[-1] = end_point
100
+ else:
101
+ # Otherwise append as usual
102
+ simplified_points.append(end_point)
103
+ simplified_points = np.array(simplified_points)
104
+
105
+ # Prepare coordinates for spline fitting
106
+ if len(simplified_points) < 2:
107
+ return path_points.copy()
108
+
109
+ x = simplified_points[:, 2] # x coordinates (scaled)
110
+ y = simplified_points[:, 1] # y coordinates (scaled)
111
+ z = simplified_points[:, 0] # z coordinates (scaled)
112
+
113
+ # Fit B-spline (k=3 for cubic, k=min(3, len-1) for short paths)
114
+ # Use a smaller k if we simplified too much
115
+ k = min(3, len(simplified_points) - 1)
116
+
117
+ # If too few points for cubic spline, just use linear interpolation or original points
118
+ if k < 1:
119
+ return path_points.copy()
120
+
121
+ # Adjust smoothing factor for simplified path
122
+ # Since we have fewer points, we need less smoothing 's'
123
+ # Original s was based on full path length.
124
+ if smoothing_factor is None:
125
+ # Re-calculate s based on simplified length
126
+ path_length = len(simplified_points)
127
+ anisotropy_factor = max(spacing_xyz) / min(spacing_xyz)
128
+ smoothing_factor = max(0, path_length - np.sqrt(2 * path_length)) * anisotropy_factor
129
+
130
+ tck, u = splprep([x, y, z], s=smoothing_factor, k=k)
131
+
132
+ # Generate smoothed points
133
+ u_new = np.linspace(0, 1, num_points)
134
+ x_smooth, y_smooth, z_smooth = splev(u_new, tck)
135
+
136
+ # Scale back to voxel coordinates
137
+ x_smooth /= spacing_xyz[0] # Unscale X
138
+ y_smooth /= spacing_xyz[1] # Unscale Y
139
+ z_smooth /= spacing_xyz[2] # Unscale Z
140
+
141
+ # Combine back to [z, y, x] format
142
+ smoothed_path = np.column_stack([z_smooth, y_smooth, x_smooth])
143
+
144
+ return smoothed_path
145
+
146
+ except Exception as e:
147
+ print(f"B-spline smoothing failed: {e}, falling back to original path")
148
+ return path_points.copy()
149
+
150
+
151
+ class PathTracingWidget(QWidget):
152
+ """Widget for tracing the brightest path ."""
153
+
154
+ # Define signals
155
+ path_created = Signal(str, str, object) # path_id, path_name, path_data
156
+ path_updated = Signal(str, str, object) # path_id, path_name, path_data
157
+
158
+ def __init__(self, viewer, image, state, scaler, scaling_update_callback):
159
+ """Initialize the path tracing widget.
160
+
161
+ Parameters:
162
+ -----------
163
+ viewer : napari.Viewer
164
+ The napari viewer instance
165
+ image : numpy.ndarray
166
+ 3D or higher-dimensional image data (potentially scaled)
167
+ state : dict
168
+ Shared state dictionary between modules
169
+ scaler : AnisotropicScaler
170
+ The scaler instance for coordinate conversion
171
+ scaling_update_callback : function
172
+ Callback function when scaling is updated
173
+ """
174
+ super().__init__()
175
+ self.viewer = viewer
176
+ self.image = image
177
+ self.state = state
178
+ self.scaler = scaler
179
+ self.scaling_update_callback = scaling_update_callback
180
+
181
+ # List to store waypoints as they are clicked
182
+ self.clicked_points = []
183
+
184
+ # Settings for path finding
185
+ self.next_path_number = 1
186
+ self.color_idx = 0
187
+
188
+ # Flag to prevent recursive event handling
189
+ self.handling_event = False
190
+
191
+ # Initialize path smoother
192
+ self.path_smoother = PathSmoother()
193
+
194
+ # Setup UI
195
+ self.setup_ui()
196
+
197
+ def setup_ui(self):
198
+ """Create the UI panel with controls"""
199
+ layout = QVBoxLayout()
200
+ layout.setSpacing(2)
201
+ layout.setContentsMargins(2, 2, 2, 2)
202
+ self.setLayout(layout)
203
+
204
+ # Main instruction
205
+ title = QLabel("<b>Path Tracing</b>")
206
+ layout.addWidget(title)
207
+
208
+ # Instructions section
209
+ instructions_section = QWidget()
210
+ instructions_layout = QVBoxLayout()
211
+ instructions_layout.setSpacing(2)
212
+ instructions_layout.setContentsMargins(2, 2, 2, 2)
213
+ instructions_section.setLayout(instructions_layout)
214
+
215
+ instructions = QLabel(
216
+ "<b>Step 1: Configure Voxel Spacing</b><br>"
217
+ "Set correct X, Y, Z spacing, then click 'Apply Scaling'<br><br>"
218
+ "<b>Step 2: Trace Paths</b><br>"
219
+ "1. Click points on dendrite structure<br>"
220
+ "2. Click 'Find Path' to trace<br>"
221
+ "3. Use 'Trace Another Path' for additional paths"
222
+ )
223
+ instructions.setWordWrap(True)
224
+ instructions_layout.addWidget(instructions)
225
+
226
+ layout.addWidget(instructions_section)
227
+
228
+ # Add separator
229
+ separator0 = QFrame()
230
+ separator0.setFrameShape(QFrame.HLine)
231
+ separator0.setFrameShadow(QFrame.Sunken)
232
+ layout.addWidget(separator0)
233
+
234
+ # Anisotropic scaling section
235
+ self._add_scaling_controls(layout)
236
+
237
+ # Add separator
238
+ separator1 = QFrame()
239
+ separator1.setFrameShape(QFrame.HLine)
240
+ separator1.setFrameShadow(QFrame.Sunken)
241
+ layout.addWidget(separator1)
242
+
243
+ # Current spacing display
244
+ spacing_info_section = QWidget()
245
+ spacing_info_layout = QVBoxLayout()
246
+ spacing_info_layout.setSpacing(2)
247
+ spacing_info_layout.setContentsMargins(2, 2, 2, 2)
248
+ spacing_info_section.setLayout(spacing_info_layout)
249
+
250
+ self.spacing_info_label = QLabel("Current voxel spacing: Not set")
251
+ self.spacing_info_label.setStyleSheet("font-weight: bold; color: #0066cc;")
252
+ spacing_info_layout.addWidget(self.spacing_info_label)
253
+
254
+ layout.addWidget(spacing_info_section)
255
+
256
+ # Update spacing display
257
+ self._update_spacing_display()
258
+
259
+ # Waypoint controls section
260
+ waypoints_section = QWidget()
261
+ waypoints_layout = QVBoxLayout()
262
+ waypoints_layout.setSpacing(2)
263
+ waypoints_layout.setContentsMargins(2, 2, 2, 2)
264
+ waypoints_section.setLayout(waypoints_layout)
265
+
266
+ self.select_waypoints_btn = QPushButton("Start Point Selection")
267
+ self.select_waypoints_btn.setFixedHeight(22)
268
+ self.select_waypoints_btn.clicked.connect(self.activate_waypoints_layer)
269
+ waypoints_layout.addWidget(self.select_waypoints_btn)
270
+
271
+ self.waypoints_status = QLabel("Status: Click to start selecting points")
272
+ waypoints_layout.addWidget(self.waypoints_status)
273
+ layout.addWidget(waypoints_section)
274
+
275
+ # Add separator
276
+ separator = QFrame()
277
+ separator.setFrameShape(QFrame.HLine)
278
+ separator.setFrameShadow(QFrame.Sunken)
279
+ layout.addWidget(separator)
280
+
281
+ # Fast algorithm settings
282
+ algorithm_section = QWidget()
283
+ algorithm_layout = QVBoxLayout()
284
+ algorithm_layout.setSpacing(2)
285
+ algorithm_layout.setContentsMargins(2, 2, 2, 2)
286
+ algorithm_section.setLayout(algorithm_layout)
287
+
288
+ # Parallel processing checkbox
289
+ self.enable_parallel_cb = QCheckBox("Enable Parallel Processing")
290
+ self.enable_parallel_cb.setChecked(True)
291
+ self.enable_parallel_cb.setToolTip("Use parallel processing for faster pathfinding")
292
+ algorithm_layout.addWidget(self.enable_parallel_cb)
293
+
294
+ # Weight heuristic parameter
295
+ weight_heuristic_layout = QHBoxLayout()
296
+ weight_heuristic_layout.setSpacing(2)
297
+ weight_heuristic_layout.addWidget(QLabel("Weight Heuristic:"))
298
+ self.weight_heuristic_spin = QDoubleSpinBox()
299
+ self.weight_heuristic_spin.setRange(0.1, 10.0)
300
+ self.weight_heuristic_spin.setSingleStep(0.1)
301
+ self.weight_heuristic_spin.setValue(2.0) # Default value
302
+ self.weight_heuristic_spin.setDecimals(1)
303
+ self.weight_heuristic_spin.setToolTip("Weight heuristic for A* search algorithm (higher = more heuristic-guided)")
304
+ weight_heuristic_layout.addWidget(self.weight_heuristic_spin)
305
+ algorithm_layout.addLayout(weight_heuristic_layout)
306
+
307
+ layout.addWidget(algorithm_section)
308
+
309
+ # Smoothing controls section
310
+ smoothing_section = QWidget()
311
+ smoothing_layout = QVBoxLayout()
312
+ smoothing_layout.setSpacing(2)
313
+ smoothing_layout.setContentsMargins(2, 2, 2, 2)
314
+ smoothing_section.setLayout(smoothing_layout)
315
+
316
+ # Smoothing checkbox
317
+ self.enable_smoothing_cb = QCheckBox("Enable B-spline Smoothing")
318
+ self.enable_smoothing_cb.setChecked(True)
319
+ self.enable_smoothing_cb.setToolTip("Apply B-spline smoothing that considers voxel spacing")
320
+ smoothing_layout.addWidget(self.enable_smoothing_cb)
321
+
322
+ # Smoothing factor
323
+ factor_layout = QHBoxLayout()
324
+ factor_layout.setSpacing(2)
325
+ factor_layout.addWidget(QLabel("Smoothing:"))
326
+ self.smoothing_factor_spin = QDoubleSpinBox()
327
+ self.smoothing_factor_spin.setRange(0.0, 10.0)
328
+ self.smoothing_factor_spin.setSingleStep(0.1)
329
+ self.smoothing_factor_spin.setValue(1.0)
330
+ self.smoothing_factor_spin.setToolTip("Higher values = more smoothing (0 = no smoothing)")
331
+ factor_layout.addWidget(self.smoothing_factor_spin)
332
+ smoothing_layout.addLayout(factor_layout)
333
+
334
+ layout.addWidget(smoothing_section)
335
+
336
+ # Add separator
337
+ separator2 = QFrame()
338
+ separator2.setFrameShape(QFrame.HLine)
339
+ separator2.setFrameShadow(QFrame.Sunken)
340
+ layout.addWidget(separator2)
341
+
342
+ # Action buttons
343
+ buttons_layout = QVBoxLayout()
344
+ buttons_layout.setSpacing(2)
345
+
346
+ # Main path finding button
347
+ self.find_path_btn = QPushButton("Find Path")
348
+ self.find_path_btn.setFixedHeight(26)
349
+ self.find_path_btn.setStyleSheet("font-weight: bold; background-color: #4CAF50; color: white;")
350
+ self.find_path_btn.clicked.connect(self.find_path)
351
+ self.find_path_btn.setEnabled(False)
352
+ buttons_layout.addWidget(self.find_path_btn)
353
+
354
+ layout.addLayout(buttons_layout)
355
+
356
+ # Add progress bar
357
+ from qtpy.QtWidgets import QProgressBar
358
+ self.progress_bar = QProgressBar()
359
+ self.progress_bar.setRange(0, 100)
360
+ self.progress_bar.setValue(0)
361
+ self.progress_bar.setTextVisible(True)
362
+ self.progress_bar.setVisible(False)
363
+ layout.addWidget(self.progress_bar)
364
+
365
+ # Management buttons
366
+ management_layout = QHBoxLayout()
367
+ management_layout.setSpacing(2)
368
+
369
+ self.trace_another_btn = QPushButton("Trace Another Path")
370
+ self.trace_another_btn.setFixedHeight(22)
371
+ self.trace_another_btn.clicked.connect(self.trace_another_path)
372
+ self.trace_another_btn.setEnabled(False)
373
+ management_layout.addWidget(self.trace_another_btn)
374
+
375
+ self.clear_points_btn = QPushButton("Clear All Points")
376
+ self.clear_points_btn.setFixedHeight(22)
377
+ self.clear_points_btn.clicked.connect(self.clear_points)
378
+ management_layout.addWidget(self.clear_points_btn)
379
+
380
+ layout.addLayout(management_layout)
381
+
382
+ # Status messages
383
+ self.status_label = QLabel("")
384
+ layout.addWidget(self.status_label)
385
+
386
+ self.error_status = QLabel("")
387
+ self.error_status.setStyleSheet("color: red;")
388
+ layout.addWidget(self.error_status)
389
+
390
+ def _add_scaling_controls(self, layout):
391
+ """Add anisotropic scaling controls to the layout"""
392
+ from qtpy.QtWidgets import (QGroupBox, QDoubleSpinBox, QComboBox, QCheckBox)
393
+
394
+ # Scaling section
395
+ scaling_group = QGroupBox("Voxel Spacing")
396
+ scaling_layout = QVBoxLayout()
397
+ scaling_layout.setSpacing(2)
398
+ scaling_layout.setContentsMargins(5, 5, 5, 5)
399
+
400
+ # Instructions
401
+ info_label = QLabel("Set voxel spacing in nanometers (will reshape the dataset):")
402
+ info_label.setWordWrap(True)
403
+ scaling_layout.addWidget(info_label)
404
+
405
+ # X spacing
406
+ x_layout = QHBoxLayout()
407
+ x_layout.addWidget(QLabel("X spacing:"))
408
+ self.x_spacing_spin = QDoubleSpinBox()
409
+ self.x_spacing_spin.setRange(1.0, 10000.0)
410
+ self.x_spacing_spin.setSingleStep(1.0)
411
+ self.x_spacing_spin.setValue(self.scaler.current_spacing_xyz[0])
412
+ self.x_spacing_spin.setDecimals(1)
413
+ self.x_spacing_spin.setSuffix(" nm")
414
+ self.x_spacing_spin.setToolTip("X-axis voxel spacing in nanometers")
415
+ self.x_spacing_spin.valueChanged.connect(self._on_spacing_changed)
416
+ x_layout.addWidget(self.x_spacing_spin)
417
+ scaling_layout.addLayout(x_layout)
418
+
419
+ # Y spacing
420
+ y_layout = QHBoxLayout()
421
+ y_layout.addWidget(QLabel("Y spacing:"))
422
+ self.y_spacing_spin = QDoubleSpinBox()
423
+ self.y_spacing_spin.setRange(1.0, 10000.0)
424
+ self.y_spacing_spin.setSingleStep(1.0)
425
+ self.y_spacing_spin.setValue(self.scaler.current_spacing_xyz[1])
426
+ self.y_spacing_spin.setDecimals(1)
427
+ self.y_spacing_spin.setSuffix(" nm")
428
+ self.y_spacing_spin.setToolTip("Y-axis voxel spacing in nanometers")
429
+ self.y_spacing_spin.valueChanged.connect(self._on_spacing_changed)
430
+ y_layout.addWidget(self.y_spacing_spin)
431
+ scaling_layout.addLayout(y_layout)
432
+
433
+ # Z spacing
434
+ z_layout = QHBoxLayout()
435
+ z_layout.addWidget(QLabel("Z spacing:"))
436
+ self.z_spacing_spin = QDoubleSpinBox()
437
+ self.z_spacing_spin.setRange(1.0, 10000.0)
438
+ self.z_spacing_spin.setSingleStep(1.0)
439
+ self.z_spacing_spin.setValue(self.scaler.current_spacing_xyz[2])
440
+ self.z_spacing_spin.setDecimals(1)
441
+ self.z_spacing_spin.setSuffix(" nm")
442
+ self.z_spacing_spin.setToolTip("Z-axis voxel spacing in nanometers")
443
+ self.z_spacing_spin.valueChanged.connect(self._on_spacing_changed)
444
+ z_layout.addWidget(self.z_spacing_spin)
445
+ scaling_layout.addLayout(z_layout)
446
+
447
+ # Interpolation method
448
+ interp_layout = QHBoxLayout()
449
+ interp_layout.addWidget(QLabel("Interpolation:"))
450
+ self.interp_combo = QComboBox()
451
+ self.interp_combo.addItems(["Nearest", "Linear", "Cubic"])
452
+ self.interp_combo.setCurrentIndex(1) # Default to linear
453
+ self.interp_combo.setToolTip("Interpolation method for scaling")
454
+ interp_layout.addWidget(self.interp_combo)
455
+ scaling_layout.addLayout(interp_layout)
456
+
457
+ # Auto-update checkbox
458
+ self.auto_update_cb = QCheckBox("Auto-update on change")
459
+ self.auto_update_cb.setChecked(False)
460
+ self.auto_update_cb.setToolTip("Automatically apply scaling when values change")
461
+ scaling_layout.addWidget(self.auto_update_cb)
462
+
463
+ # Control buttons
464
+ button_layout = QHBoxLayout()
465
+
466
+ self.apply_scaling_btn = QPushButton("Apply Scaling")
467
+ self.apply_scaling_btn.setToolTip("Apply current scaling settings to reshape the image")
468
+ self.apply_scaling_btn.setFixedHeight(22)
469
+ self.apply_scaling_btn.clicked.connect(self._apply_scaling)
470
+ button_layout.addWidget(self.apply_scaling_btn)
471
+
472
+ self.reset_scaling_btn = QPushButton("Reset to Original")
473
+ self.reset_scaling_btn.setToolTip("Reset to original voxel spacing")
474
+ self.reset_scaling_btn.setFixedHeight(22)
475
+ self.reset_scaling_btn.clicked.connect(self._reset_scaling)
476
+ button_layout.addWidget(self.reset_scaling_btn)
477
+
478
+ scaling_layout.addLayout(button_layout)
479
+
480
+ # Status info
481
+ self.scaling_status = QLabel("Status: Original spacing")
482
+ self.scaling_status.setWordWrap(True)
483
+ self.scaling_status.setStyleSheet("font-weight: bold; color: #0066cc;")
484
+ scaling_layout.addWidget(self.scaling_status)
485
+
486
+ scaling_group.setLayout(scaling_layout)
487
+ layout.addWidget(scaling_group)
488
+
489
+ # Update initial status
490
+ self._update_status_only()
491
+
492
+ def _on_spacing_changed(self):
493
+ """Handle when spacing values change"""
494
+ if self.auto_update_cb.isChecked():
495
+ self._apply_scaling()
496
+ else:
497
+ self._update_status_only()
498
+
499
+ def _update_status_only(self):
500
+ """Update status without applying scaling"""
501
+ x_nm = self.x_spacing_spin.value()
502
+ y_nm = self.y_spacing_spin.value()
503
+ z_nm = self.z_spacing_spin.value()
504
+
505
+ # Calculate what the scale factors would be
506
+ temp_scale_factors = np.array([
507
+ self.scaler.original_spacing_xyz[2] / z_nm, # Z
508
+ self.scaler.original_spacing_xyz[1] / y_nm, # Y
509
+ self.scaler.original_spacing_xyz[0] / x_nm # X
510
+ ])
511
+
512
+ self.scaling_status.setText(
513
+ f"Pending: X={x_nm:.1f}, Y={y_nm:.1f}, Z={z_nm:.1f} nm\n"
514
+ f"Scale factors (Z,Y,X): {temp_scale_factors[0]:.3f}, {temp_scale_factors[1]:.3f}, {temp_scale_factors[2]:.3f}"
515
+ )
516
+
517
+ def _apply_scaling(self):
518
+ """Apply current scaling settings"""
519
+ try:
520
+ x_nm = self.x_spacing_spin.value()
521
+ y_nm = self.y_spacing_spin.value()
522
+ z_nm = self.z_spacing_spin.value()
523
+
524
+ # Update scaler
525
+ self.scaler.set_spacing(x_nm, y_nm, z_nm)
526
+
527
+ # Get interpolation order
528
+ interp_order = self.interp_combo.currentIndex()
529
+ if interp_order == 0:
530
+ order = 0 # Nearest
531
+ elif interp_order == 1:
532
+ order = 1 # Linear
533
+ else:
534
+ order = 3 # Cubic
535
+
536
+ # Update status
537
+ volume_ratio = self.scaler.get_volume_ratio()
538
+ self.scaling_status.setText(
539
+ f"Applied: X={x_nm:.1f}, Y={y_nm:.1f}, Z={z_nm:.1f} nm\n"
540
+ f"Scale factors (Z,Y,X): {self.scaler.scale_factors[0]:.3f}, {self.scaler.scale_factors[1]:.3f}, {self.scaler.scale_factors[2]:.3f}\n"
541
+ f"Volume ratio: {volume_ratio:.3f}"
542
+ )
543
+
544
+ # Call the main widget's scaling update callback
545
+ if self.scaling_update_callback:
546
+ self.scaling_update_callback(order)
547
+
548
+ napari.utils.notifications.show_info(f"Applied scaling: X={x_nm:.1f}, Y={y_nm:.1f}, Z={z_nm:.1f} nm")
549
+
550
+ except Exception as e:
551
+ napari.utils.notifications.show_info(f"Error applying scaling: {str(e)}")
552
+ print(f"Scaling error: {str(e)}")
553
+
554
+ def _reset_scaling(self):
555
+ """Reset to original scaling"""
556
+ self.scaler.reset_to_original()
557
+
558
+ # Update UI
559
+ self.x_spacing_spin.setValue(self.scaler.current_spacing_xyz[0])
560
+ self.y_spacing_spin.setValue(self.scaler.current_spacing_xyz[1])
561
+ self.z_spacing_spin.setValue(self.scaler.current_spacing_xyz[2])
562
+
563
+ self.scaling_status.setText("Status: Reset to original spacing")
564
+
565
+ # Call the main widget's scaling update callback
566
+ if self.scaling_update_callback:
567
+ self.scaling_update_callback(1) # Linear interpolation for reset
568
+
569
+ napari.utils.notifications.show_info("Reset to original voxel spacing")
570
+
571
+ def _update_spacing_display(self):
572
+ """Update the spacing display with current values"""
573
+ try:
574
+ if 'current_spacing_xyz' in self.state:
575
+ spacing = self.state['current_spacing_xyz']
576
+ self.spacing_info_label.setText(
577
+ f"Current voxel spacing: X={spacing[0]:.1f}, Y={spacing[1]:.1f}, Z={spacing[2]:.1f} nm"
578
+ )
579
+ else:
580
+ self.spacing_info_label.setText("Current voxel spacing: Not configured")
581
+ except Exception as e:
582
+ self.spacing_info_label.setText("Current voxel spacing: Error reading values")
583
+ print(f"Error updating spacing display: {e}")
584
+
585
+ def activate_waypoints_layer(self):
586
+ """Activate the waypoints layer for selecting points"""
587
+ if self.handling_event:
588
+ return
589
+
590
+ try:
591
+ self.handling_event = True
592
+ self.viewer.layers.selection.active = self.state['waypoints_layer']
593
+ self.error_status.setText("")
594
+ self.status_label.setText("Click points on the dendrite structure")
595
+ self._update_spacing_display() # Update spacing display
596
+ napari.utils.notifications.show_info("Click points on the dendrite")
597
+ except Exception as e:
598
+ error_msg = f"Error activating waypoints layer: {str(e)}"
599
+ napari.utils.notifications.show_info(error_msg)
600
+ self.error_status.setText(error_msg)
601
+ finally:
602
+ self.handling_event = False
603
+
604
+ def on_waypoints_changed(self, event=None):
605
+ """Handle when waypoints are added or changed"""
606
+ if self.handling_event:
607
+ return
608
+
609
+ try:
610
+ self.handling_event = True
611
+
612
+ waypoints_layer = self.state['waypoints_layer']
613
+ if len(waypoints_layer.data) > 0:
614
+ # Validate points are within image bounds
615
+ valid_points = []
616
+ for point in waypoints_layer.data:
617
+ valid = True
618
+ for i, coord in enumerate(point):
619
+ if coord < 0 or coord >= self.image.shape[i]:
620
+ valid = False
621
+ break
622
+
623
+ if valid:
624
+ valid_points.append(point)
625
+
626
+ # Update the waypoints layer with only valid points
627
+ if len(valid_points) != len(waypoints_layer.data):
628
+ waypoints_layer.data = np.array(valid_points)
629
+ napari.utils.notifications.show_info("Some points were outside image bounds and were removed.")
630
+
631
+ # Convert to integer coordinates and store
632
+ self.clicked_points = [point.astype(int) for point in valid_points]
633
+
634
+ # Update status
635
+ num_points = len(self.clicked_points)
636
+ self.waypoints_status.setText(f"Status: {num_points} points selected")
637
+
638
+ # Enable buttons if we have enough points
639
+ self.find_path_btn.setEnabled(num_points >= 2)
640
+
641
+ if num_points >= 2:
642
+ self.status_label.setText("Ready to find path!")
643
+ else:
644
+ self.status_label.setText(f"Need at least 2 points (currently have {num_points})")
645
+ else:
646
+ self.clicked_points = []
647
+ self.waypoints_status.setText("Status: Click to start selecting points")
648
+ self.find_path_btn.setEnabled(False)
649
+ self.status_label.setText("")
650
+ except Exception as e:
651
+ napari.utils.notifications.show_info(f"Error processing waypoints: {str(e)}")
652
+ print(f"Error details: {str(e)}")
653
+ finally:
654
+ self.handling_event = False
655
+
656
+ def find_path(self):
657
+ """Find path using the custom A* algorithm"""
658
+ if self.handling_event:
659
+ return
660
+
661
+ try:
662
+ if len(self.clicked_points) < 2:
663
+ napari.utils.notifications.show_info("Please select at least 2 points")
664
+ self.error_status.setText("Error: Please select at least 2 points")
665
+ return
666
+
667
+ # Lock UI
668
+ self.handling_event = True
669
+ self.find_path_btn.setEnabled(False)
670
+ self.progress_bar.setVisible(True)
671
+ self.progress_bar.setValue(0)
672
+ self.error_status.setText("")
673
+
674
+ # Prepare data
675
+ points_list = [point.tolist() for point in self.clicked_points]
676
+ enable_parallel = self.enable_parallel_cb.isChecked()
677
+ weight_heuristic = self.weight_heuristic_spin.value()
678
+ enable_smoothing = self.enable_smoothing_cb.isChecked()
679
+ smoothing_factor = self.smoothing_factor_spin.value()
680
+ spacing_xyz = self.state.get('current_spacing_xyz', (1.0, 1.0, 1.0))
681
+
682
+ from napari.qt.threading import thread_worker
683
+
684
+ @thread_worker
685
+ def path_worker():
686
+ yield (10, "Initializing search...")
687
+
688
+ # Run the search
689
+ yield (20, "Tracing bright path (A*)...")
690
+ # Note: quick_accurate_optimized_search is still blocking/long-running here
691
+ # but since we are in a thread, UI stays responsive-ish (indeterminate)
692
+ path = quick_accurate_optimized_search(
693
+ image=self.image,
694
+ points_list=points_list,
695
+ verbose=False,
696
+ enable_parallel=enable_parallel,
697
+ my_weight_heuristic=weight_heuristic
698
+ )
699
+
700
+ yield (80, "Path found. Post-processing...")
701
+
702
+ if path is not None and len(path) > 0:
703
+ path_data = np.array(path)
704
+
705
+ if enable_smoothing and len(path_data) >= 3 and smoothing_factor > 0:
706
+ yield (90, "Applying B-spline smoothing...")
707
+ path_data = self.path_smoother.smooth_path(
708
+ path_data,
709
+ spacing_xyz=spacing_xyz,
710
+ smoothing_factor=smoothing_factor,
711
+ preserve_endpoints=True
712
+ )
713
+
714
+ return path_data
715
+ return None
716
+
717
+ def on_yield(data):
718
+ progress, status = data
719
+ self.progress_bar.setValue(progress)
720
+ self.status_label.setText(status)
721
+
722
+ def on_return(path_data):
723
+ self.progress_bar.setValue(100)
724
+ self.progress_bar.setVisible(False)
725
+ self.handling_event = False
726
+ self.find_path_btn.setEnabled(True)
727
+
728
+ if path_data is None:
729
+ self.status_label.setText("No path found.")
730
+ napari.utils.notifications.show_warning("No path found.")
731
+ else:
732
+ self.status_label.setText("Path complete!")
733
+ self._finalize_path(path_data)
734
+
735
+ def on_error(e):
736
+ self.handling_event = False
737
+ self.find_path_btn.setEnabled(True)
738
+ self.progress_bar.setVisible(False)
739
+ self.progress_bar.setValue(0)
740
+ self.status_label.setText("Error during tracing")
741
+ self.error_status.setText(f"Error: {str(e)}")
742
+ napari.utils.notifications.show_error(f"Tracing failed: {e}")
743
+ print(f"Tracing error: {e}")
744
+
745
+ # Start worker
746
+ worker = path_worker()
747
+ worker.yielded.connect(on_yield)
748
+ worker.returned.connect(on_return)
749
+ worker.errored.connect(on_error)
750
+ worker.start()
751
+
752
+ except Exception as e:
753
+ self.handling_event = False
754
+ self.find_path_btn.setEnabled(True)
755
+ self.progress_bar.setVisible(False)
756
+ print(f"Setup error: {e}")
757
+
758
+ def _finalize_path(self, path_data):
759
+ """Handle successful path creation (moved from main logic)"""
760
+ try:
761
+ # Generate path name
762
+ path_name = f"Path {self.next_path_number}"
763
+ self.next_path_number += 1
764
+
765
+ # Get color for this path
766
+ path_color = self.get_next_color()
767
+
768
+ # Create a new layer for this path
769
+ path_layer = self.viewer.add_points(
770
+ path_data,
771
+ name=path_name,
772
+ size=3,
773
+ face_color=path_color,
774
+ opacity=0.8
775
+ )
776
+
777
+ # Update 3D visualization if applicable
778
+ if self.image.ndim > 2 and self.state['traced_path_layer'] is not None:
779
+ self._update_traced_path_visualization(path_data)
780
+
781
+ # Generate a unique ID for this path
782
+ path_id = str(uuid.uuid4())
783
+
784
+ # Store the path with enhanced metadata
785
+ current_spacing = self.state.get('current_spacing_xyz', (1.0, 1.0, 1.0))
786
+
787
+ # Retrieve current settings for metadata
788
+ enable_parallel = self.enable_parallel_cb.isChecked()
789
+ weight_heuristic = self.weight_heuristic_spin.value()
790
+ current_spacing = self.state.get('current_spacing_xyz', (1.0, 1.0, 1.0))
791
+
792
+ self.state['paths'][path_id] = {
793
+ 'name': path_name,
794
+ 'data': path_data,
795
+ 'start': self.clicked_points[0].copy(),
796
+ 'end': self.clicked_points[-1].copy(),
797
+ 'waypoints': [point.copy() for point in self.clicked_points[1:-1]] if len(self.clicked_points) > 2 else [],
798
+ 'visible': True,
799
+ 'layer': path_layer,
800
+ 'original_clicks': [point.copy() for point in self.clicked_points],
801
+ 'smoothed': self.enable_smoothing_cb.isChecked() and self.smoothing_factor_spin.value() > 0,
802
+ 'algorithm': 'waypoint_astar',
803
+ 'parallel_processing': enable_parallel,
804
+ 'weight_heuristic': weight_heuristic, # Store weight heuristic parameter
805
+ 'voxel_spacing_xyz': current_spacing, # Store voxel spacing with path
806
+ 'anisotropic_smoothing': self.enable_smoothing_cb.isChecked()
807
+ }
808
+
809
+ # Store reference to the layer
810
+ self.state['path_layers'][path_id] = path_layer
811
+
812
+ # Update UI
813
+ algorithm_info = f" (parallel, weight={weight_heuristic:.1f})" if enable_parallel else f" (sequential, weight={weight_heuristic:.1f})"
814
+ smoothing_msg = " (smoothing)" if self.state['paths'][path_id]['smoothed'] else ""
815
+ spacing_info = f" at {current_spacing[0]:.1f}, {current_spacing[1]:.1f}, {current_spacing[2]:.1f} nm"
816
+
817
+ msg = f"Path found: {len(path_data)} points {spacing_info}"
818
+ napari.utils.notifications.show_info(msg)
819
+ self.status_label.setText(f"Success: {path_name} created")
820
+
821
+ # Enable trace another path button
822
+ self.trace_another_btn.setEnabled(True)
823
+
824
+ # Store current path ID in state
825
+ self.state['current_path_id'] = path_id
826
+
827
+ # Emit signal that a new path was created
828
+ self.path_created.emit(path_id, path_name, path_data)
829
+
830
+ except Exception as e:
831
+ msg = f"Error finalizing path: {e}"
832
+ napari.utils.notifications.show_error(msg)
833
+ print(f"Finalize error: {e}")
834
+
835
+ def _update_traced_path_visualization(self, path):
836
+ """Update the 3D traced path visualization"""
837
+ if self.state['traced_path_layer'] is None:
838
+ return
839
+
840
+ try:
841
+ # Get the z-range of the path
842
+ path_array = np.array(path)
843
+ z_values = [point[0] for point in path]
844
+ min_z = int(min(z_values))
845
+ max_z = int(max(z_values))
846
+
847
+ # Create a projection of the path onto every frame in the range
848
+ traced_points = []
849
+ for z in range(min_z, max_z + 1):
850
+ for point in path:
851
+ new_point = point.copy()
852
+ new_point[0] = z
853
+ traced_points.append(new_point)
854
+
855
+ # Update the traced path layer
856
+ if traced_points:
857
+ self.state['traced_path_layer'].data = np.array(traced_points)
858
+ self.state['traced_path_layer'].visible = True
859
+ self.viewer.dims.set_point(0, min_z)
860
+ except Exception as e:
861
+ print(f"Error updating traced path visualization: {e}")
862
+
863
+ def trace_another_path(self):
864
+ """Reset for tracing a new path while preserving existing paths"""
865
+ # Clear current points
866
+ self.clicked_points = []
867
+ self.state['waypoints_layer'].data = np.empty((0, self.image.ndim))
868
+
869
+ # Reset UI for new path
870
+ self.waypoints_status.setText("Status: Click to start selecting points")
871
+ self.status_label.setText("Ready for new path - click points on dendrite")
872
+ self.find_path_btn.setEnabled(False)
873
+ self.trace_another_btn.setEnabled(False)
874
+
875
+ # Update spacing display
876
+ self._update_spacing_display()
877
+
878
+ # Activate the waypoints layer for the new path
879
+ self.viewer.layers.selection.active = self.state['waypoints_layer']
880
+ napari.utils.notifications.show_info("Ready to trace a new path. Click points on the dendrite.")
881
+
882
+ def clear_points(self):
883
+ """Clear all waypoints and paths"""
884
+ self.clicked_points = []
885
+ self.state['waypoints_layer'].data = np.empty((0, self.image.ndim))
886
+
887
+ # Clear traced path layer if it exists
888
+ if self.state['traced_path_layer'] is not None:
889
+ self.state['traced_path_layer'].data = np.empty((0, self.image.ndim))
890
+ self.state['traced_path_layer'].visible = False
891
+
892
+ # Reset UI
893
+ self.waypoints_status.setText("Status: Click to start selecting points")
894
+ self.status_label.setText("")
895
+ self.error_status.setText("")
896
+
897
+ # Reset buttons
898
+ self.find_path_btn.setEnabled(False)
899
+ self.trace_another_btn.setEnabled(False)
900
+
901
+ # Update spacing display
902
+ self._update_spacing_display()
903
+
904
+ napari.utils.notifications.show_info("All points cleared. Ready to start over.")
905
+
906
+ def get_next_color(self):
907
+ """Get the next color from the predefined list"""
908
+ colors = ['cyan', 'magenta', 'green', 'blue', 'orange',
909
+ 'purple', 'teal', 'coral', 'gold', 'lavender']
910
+
911
+ color = colors[self.color_idx % len(colors)]
912
+ self.color_idx += 1
913
+
914
+ return color
915
+
916
+ def load_path_waypoints(self, path_id):
917
+ """Load the waypoints for a specific path"""
918
+ if self.handling_event:
919
+ return
920
+
921
+ try:
922
+ self.handling_event = True
923
+
924
+ if path_id not in self.state['paths']:
925
+ return
926
+
927
+ path_data = self.state['paths'][path_id]
928
+
929
+ # Check if this path has original clicks stored
930
+ if ('original_clicks' in path_data and
931
+ len(path_data['original_clicks']) > 0):
932
+ # Load the original clicked points
933
+ self.clicked_points = [np.array(point) for point in path_data['original_clicks']]
934
+
935
+ # Update the waypoints layer
936
+ if self.clicked_points:
937
+ self.state['waypoints_layer'].data = np.array(self.clicked_points)
938
+ self.waypoints_status.setText(f"Status: {len(self.clicked_points)} points loaded")
939
+ elif ('original_clicks' in path_data and
940
+ len(path_data['original_clicks']) == 0):
941
+ # This is a connected path - show a subset of the path points as waypoints
942
+ path_points = path_data['data']
943
+ if len(path_points) > 10:
944
+ # Take every nth point to get about 10 waypoints
945
+ step = len(path_points) // 10
946
+ waypoint_indices = range(0, len(path_points), step)
947
+ new_waypoints = [path_points[i] for i in waypoint_indices]
948
+ else:
949
+ # Use all points if path is short
950
+ new_waypoints = path_points.copy()
951
+
952
+ self.clicked_points = new_waypoints
953
+ self.state['waypoints_layer'].data = np.array(new_waypoints)
954
+ self.waypoints_status.setText(f"Status: {len(new_waypoints)} waypoints from connected path")
955
+ else:
956
+ # Fallback - reconstruct from start, waypoints, and end
957
+ new_waypoints = []
958
+ if 'start' in path_data and path_data['start'] is not None:
959
+ new_waypoints.append(path_data['start'])
960
+
961
+ if 'waypoints' in path_data and path_data['waypoints']:
962
+ new_waypoints.extend(path_data['waypoints'])
963
+
964
+ if 'end' in path_data and path_data['end'] is not None:
965
+ new_waypoints.append(path_data['end'])
966
+
967
+ # Update the waypoints layer
968
+ if new_waypoints:
969
+ self.state['waypoints_layer'].data = np.array(new_waypoints)
970
+ self.clicked_points = new_waypoints
971
+ self.waypoints_status.setText(f"Status: {len(new_waypoints)} points loaded")
972
+
973
+ # Enable buttons
974
+ if len(self.clicked_points) >= 2:
975
+ self.find_path_btn.setEnabled(True)
976
+ self.trace_another_btn.setEnabled(True)
977
+
978
+ # Clear any error messages
979
+ self.error_status.setText("")
980
+
981
+ # Show path status including algorithm type, weight heuristic, and spacing info
982
+ path_type = ""
983
+ if path_data.get('algorithm') == 'waypoint_astar':
984
+ path_type = " (waypoint_astar"
985
+ if path_data.get('parallel_processing', False):
986
+ path_type += ", parallel"
987
+ # Add weight heuristic info if available
988
+ if 'weight_heuristic' in path_data:
989
+ path_type += f", weight={path_data['weight_heuristic']:.1f}"
990
+ path_type += ")"
991
+ if path_data.get('anisotropic_smoothing', False):
992
+ path_type += " (anisotropic smoothing)"
993
+ elif path_data.get('smoothed', False):
994
+ path_type = " (smoothed)"
995
+ elif ('original_clicks' in path_data and
996
+ len(path_data['original_clicks']) == 0):
997
+ path_type = " (connected)"
998
+
999
+ # Add spacing info if available
1000
+ if 'voxel_spacing_xyz' in path_data:
1001
+ spacing = path_data['voxel_spacing_xyz']
1002
+ spacing_info = f" [X={spacing[0]:.1f}, Y={spacing[1]:.1f}, Z={spacing[2]:.1f} nm]"
1003
+ path_type += spacing_info
1004
+
1005
+ self.status_label.setText(f"Loaded path: {path_data['name']}{path_type}")
1006
+
1007
+ # Update spacing display
1008
+ self._update_spacing_display()
1009
+
1010
+ napari.utils.notifications.show_info(f"Loaded {path_data['name']}{path_type}")
1011
+ except Exception as e:
1012
+ error_msg = f"Error loading path waypoints: {str(e)}"
1013
+ napari.utils.notifications.show_info(error_msg)
1014
+ self.error_status.setText(error_msg)
1015
+ finally:
1016
+ self.handling_event = False