nettracer3d 1.1.0__py3-none-any.whl → 1.2.3__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.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

@@ -4,7 +4,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QG
4
4
  QHBoxLayout, QSlider, QMenuBar, QMenu, QDialog,
5
5
  QFormLayout, QLineEdit, QPushButton, QFileDialog,
6
6
  QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
7
- QMenu, QTabWidget, QGroupBox, QCheckBox)
7
+ QMenu, QTabWidget, QGroupBox, QCheckBox, QScrollArea)
8
8
  from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent, QEventLoop)
9
9
  import numpy as np
10
10
  import time
@@ -56,6 +56,7 @@ class ImageViewerWindow(QMainWindow):
56
56
  self.last_saved_name = None
57
57
  self.last_load = None
58
58
  self.temp_chan = 0
59
+ self.scale_bar = False
59
60
 
60
61
  self.color_dictionary = {
61
62
  # Reds
@@ -184,6 +185,26 @@ class ImageViewerWindow(QMainWindow):
184
185
  3: None
185
186
  }
186
187
 
188
+ self.surface_area_dict = {
189
+ 0: None,
190
+ 1: None,
191
+ 2: None,
192
+ 3: None
193
+ }
194
+
195
+ self.sphericity_dict = {
196
+ 0: None,
197
+ 1: None,
198
+ 2: None,
199
+ 3: None
200
+ }
201
+
202
+ self.branch_dict = {
203
+ 0: None,
204
+ 1: None
205
+
206
+ }
207
+
187
208
  self.original_shape = None #For undoing resamples
188
209
 
189
210
  # Create control panel
@@ -211,6 +232,14 @@ class ImageViewerWindow(QMainWindow):
211
232
  buttons_widget = QWidget()
212
233
  buttons_layout = QHBoxLayout(buttons_widget)
213
234
 
235
+
236
+ self.toggle_scale = QPushButton("📏")
237
+ self.toggle_scale.setFixedSize(20, 20)
238
+ self.toggle_scale.clicked.connect(self.toggle_scalebar)
239
+ self.toggle_scale.setCheckable(True)
240
+ self.toggle_scale.setChecked(False)
241
+ control_layout.addWidget(self.toggle_scale)
242
+
214
243
  self.reset_view = QPushButton("🏠")
215
244
  self.reset_view.setFixedSize(20, 20)
216
245
  self.reset_view.clicked.connect(self.home)
@@ -462,6 +491,7 @@ class ImageViewerWindow(QMainWindow):
462
491
 
463
492
  self.resume = False
464
493
  self._first_pan_done = False
494
+ self.thresh_window_ref = None
465
495
 
466
496
 
467
497
  def load_file(self):
@@ -879,10 +909,14 @@ class ImageViewerWindow(QMainWindow):
879
909
 
880
910
  def confirm_mini_thresh(self):
881
911
 
882
- if self.shape[0] * self.shape[1] * self.shape[2] > self.mini_thresh:
883
- self.mini_overlay = True
884
- return True
885
- else:
912
+ try:
913
+
914
+ if self.shape[0] * self.shape[1] * self.shape[2] > self.mini_thresh:
915
+ self.mini_overlay = True
916
+ return True
917
+ else:
918
+ return False
919
+ except:
886
920
  return False
887
921
 
888
922
  def evaluate_mini(self, mode = 'nodes'):
@@ -1269,7 +1303,7 @@ class ImageViewerWindow(QMainWindow):
1269
1303
  # Create context menu
1270
1304
  context_menu = QMenu(self)
1271
1305
 
1272
- find = context_menu.addAction("Find Node/Edge")
1306
+ find = context_menu.addAction("Find Node/Edge/community")
1273
1307
  find.triggered.connect(self.handle_find)
1274
1308
 
1275
1309
  # Create "Show Neighbors" submenu
@@ -1952,7 +1986,7 @@ class ImageViewerWindow(QMainWindow):
1952
1986
  class FindDialog(QDialog):
1953
1987
  def __init__(self, parent=None):
1954
1988
  super().__init__(parent)
1955
- self.setWindowTitle("Find Node (or edge?)")
1989
+ self.setWindowTitle("Find Node (or edge/com?)")
1956
1990
  self.setModal(True)
1957
1991
 
1958
1992
  layout = QFormLayout(self)
@@ -1962,7 +1996,11 @@ class ImageViewerWindow(QMainWindow):
1962
1996
 
1963
1997
  self.mode_selector = QComboBox()
1964
1998
  self.mode_selector.addItems(["nodes", "edges", "communities"])
1965
- self.mode_selector.setCurrentIndex(0) # Default to Mode 1
1999
+ if self.parent().active_channel == 1:
2000
+ self.mode_selector.setCurrentIndex(1)
2001
+ else:
2002
+ self.mode_selector.setCurrentIndex(0)
2003
+
1966
2004
  layout.addRow("Type to select:", self.mode_selector)
1967
2005
 
1968
2006
  run_button = QPushButton("Enter")
@@ -1988,6 +2026,7 @@ class ImageViewerWindow(QMainWindow):
1988
2026
  num = (self.parent().channel_data[1].shape[0] * self.parent().channel_data[1].shape[1] * self.parent().channel_data[1].shape[2])
1989
2027
 
1990
2028
  self.parent().clicked_values['edges'] = [value]
2029
+ self.parent().handle_info(sort = 'edge')
1991
2030
 
1992
2031
  if value in my_network.edge_centroids:
1993
2032
 
@@ -2017,6 +2056,7 @@ class ImageViewerWindow(QMainWindow):
2017
2056
 
2018
2057
  if mode == 0:
2019
2058
  self.parent().clicked_values['nodes'] = [value]
2059
+ self.parent().handle_info(sort = 'node')
2020
2060
  elif mode == 2:
2021
2061
 
2022
2062
  coms = n3d.invert_dict(my_network.communities)
@@ -2167,6 +2207,28 @@ class ImageViewerWindow(QMainWindow):
2167
2207
  except:
2168
2208
  pass
2169
2209
 
2210
+ if self.surface_area_dict[0] is not None:
2211
+ try:
2212
+ info_dict['~Surface Area (Scaled; Jagged Faces)'] = self.surface_area_dict[0][label]
2213
+ except:
2214
+ pass
2215
+
2216
+ if self.sphericity_dict[0] is not None:
2217
+ try:
2218
+ info_dict['Sphericity'] = self.sphericity_dict[0][label]
2219
+ except:
2220
+ pass
2221
+
2222
+ if self.branch_dict[0] is not None:
2223
+ try:
2224
+ info_dict['Branch Length'] = self.branch_dict[0][0][label]
2225
+ except:
2226
+ pass
2227
+ try:
2228
+ info_dict['Branch Tortuosity'] = self.branch_dict[0][1][label]
2229
+ except:
2230
+ pass
2231
+
2170
2232
 
2171
2233
  elif sort == 'edge':
2172
2234
 
@@ -2212,6 +2274,28 @@ class ImageViewerWindow(QMainWindow):
2212
2274
  except:
2213
2275
  pass
2214
2276
 
2277
+ if self.surface_area_dict[1] is not None:
2278
+ try:
2279
+ info_dict['~Surface Area (Scaled; Jagged Faces)'] = self.surface_area_dict[1][label]
2280
+ except:
2281
+ pass
2282
+
2283
+ if self.sphericity_dict[1] is not None:
2284
+ try:
2285
+ info_dict['Sphericity'] = self.sphericity_dict[1][label]
2286
+ except:
2287
+ pass
2288
+
2289
+ if self.branch_dict[1] is not None:
2290
+ try:
2291
+ info_dict['Branch Length'] = self.branch_dict[1][0][label]
2292
+ except:
2293
+ pass
2294
+ try:
2295
+ info_dict['Branch Tortuosity'] = self.branch_dict[1][1][label]
2296
+ except:
2297
+ pass
2298
+
2215
2299
  self.format_for_upperright_table(info_dict, title = f'Info on Object', sort = False)
2216
2300
 
2217
2301
  except:
@@ -2281,47 +2365,99 @@ class ImageViewerWindow(QMainWindow):
2281
2365
  print(f"An error has occured: {e}")
2282
2366
 
2283
2367
 
2284
- def process_single_label_bbox(args):
2285
- """
2286
- Worker function to process a single label within its bounding box
2287
- This function will run in parallel
2288
- """
2289
- label_subarray, original_label, bbox_slices, start_new_label = args
2368
+ def expand_bbox(self, bbox, array_shape, padding=1):
2369
+ """Expand bounding box by padding in each dimension, clamped to array bounds"""
2370
+ expanded = []
2371
+ for i, slice_obj in enumerate(bbox):
2372
+ start = max(0, slice_obj.start - padding)
2373
+ stop = min(array_shape[i], slice_obj.stop + padding)
2374
+ expanded.append(slice(start, stop, None))
2375
+ return tuple(expanded)
2376
+
2377
+ def process_label_split_only(self, item, input_array):
2378
+ """Pass 1: Split disconnected components, identify largest"""
2379
+ orig_label, bbox = item
2290
2380
 
2291
2381
  try:
2382
+ # Extract subarray
2383
+ label_subarray = input_array[bbox]
2384
+
2292
2385
  # Create binary mask for this label only
2293
- binary_mask = label_subarray == original_label
2386
+ binary_mask = label_subarray == orig_label
2294
2387
 
2295
2388
  if not np.any(binary_mask):
2296
- return None, start_new_label, bbox_slices
2389
+ return orig_label, bbox, None, 0, None
2297
2390
 
2298
- # Find connected components in the subarray
2391
+ # Find connected components
2299
2392
  labeled_cc, num_cc = n3d.label_objects(binary_mask)
2300
2393
 
2301
2394
  if num_cc == 0:
2302
- return None, start_new_label, bbox_slices
2395
+ return orig_label, bbox, None, 0, None
2396
+
2397
+ # Find largest component
2398
+ volumes = np.bincount(labeled_cc.ravel())[1:]
2399
+ largest_cc_id = np.argmax(volumes) + 1
2400
+
2401
+ return orig_label, bbox, labeled_cc, num_cc, largest_cc_id
2303
2402
 
2304
- # Create output subarray with new labels
2305
- output_subarray = np.zeros_like(label_subarray)
2403
+ except Exception as e:
2404
+ print(f"Error processing label {orig_label}: {e}")
2405
+ return orig_label, bbox, None, 0, None
2406
+
2407
+ def process_illegal_label_reassign(self, item, pass1_array, original_max_val):
2408
+ """Pass 2: Reassign illegal labels based on legal neighbors"""
2409
+ illegal_label, bbox = item
2410
+
2411
+ try:
2412
+ # Expand bbox by 1
2413
+ expanded_bbox = self.expand_bbox(bbox, pass1_array.shape, padding=1)
2414
+
2415
+ # Extract subarray
2416
+ subarray = pass1_array[expanded_bbox]
2417
+
2418
+ # Create mask for this illegal label
2419
+ illegal_mask = subarray == illegal_label
2420
+
2421
+ if not np.any(illegal_mask):
2422
+ return illegal_label, None
2423
+
2424
+ # Dilate to find neighbors
2425
+ dilated_mask = n3d.dilate_3D_old(illegal_mask, 3, 3, 3)
2306
2426
 
2307
- # Assign new consecutive labels starting from start_new_label
2308
- for cc_id in range(1, num_cc + 1):
2309
- cc_mask = labeled_cc == cc_id
2310
- new_label = start_new_label + cc_id - 1
2311
- output_subarray[cc_mask] = new_label
2427
+ # Border region
2428
+ border_mask = dilated_mask & ~illegal_mask & (subarray > 0)
2312
2429
 
2313
- # Return the processed subarray, number of components created, and bbox info
2314
- return output_subarray, start_new_label + num_cc, bbox_slices
2430
+ # Get border labels
2431
+ border_labels = subarray * border_mask
2432
+
2433
+ # Filter out illegal labels (> original_max_val) and background (0)
2434
+ legal_border_labels = border_labels.copy()
2435
+ legal_border_labels[border_labels > original_max_val] = 0
2436
+
2437
+ # Count occurrences of legal neighbors
2438
+ unique_borders = np.bincount(legal_border_labels.ravel())[1:] # Skip 0
2439
+
2440
+ if len(unique_borders) > 0 and np.max(unique_borders) > 0:
2441
+ # Found legal neighbors, pick largest shared border
2442
+ chosen_label = np.argmax(unique_borders) + 1
2443
+ return illegal_label, chosen_label
2444
+ else:
2445
+ # No legal neighbors, keep current label
2446
+ return illegal_label, None
2315
2447
 
2316
2448
  except Exception as e:
2317
- print(f"Error processing label {original_label}: {e}")
2318
- return None, start_new_label, bbox_slices
2449
+ print(f"Error processing illegal label {illegal_label}: {e}")
2450
+ return illegal_label, None
2319
2451
 
2320
- def separate_nontouching_objects(self, input_array, max_val=0):
2452
+ def separate_nontouching_objects(self, input_array, max_val=None, branches=False):
2321
2453
  """
2322
- Ultra-optimized version using find_objects directly without remapping
2454
+ Two-pass algorithm:
2455
+ Pass 1: Split disconnected components (largest keeps label, others get new labels)
2456
+ Pass 2 (branches=True only): Reassign new labels based on legal neighbors
2323
2457
  """
2324
- print("Splitting nontouching objects")
2458
+ if max_val == None:
2459
+ max_val = np.max(input_array)
2460
+ print("Splitting nontouching objects - Pass 1")
2325
2461
 
2326
2462
  binary_mask = input_array > 0
2327
2463
  if not np.any(binary_mask):
@@ -2330,13 +2466,15 @@ class ImageViewerWindow(QMainWindow):
2330
2466
  unique_labels = np.unique(input_array[binary_mask])
2331
2467
  print(f"Processing {len(unique_labels)} unique labels")
2332
2468
 
2469
+ # Store original max_val for later
2470
+ original_max_val = int(max_val)
2471
+
2333
2472
  # Get all bounding boxes at once
2334
2473
  bounding_boxes = ndimage.find_objects(input_array)
2335
2474
 
2336
- # Prepare work items - just check if bounding box exists for each label
2475
+ # Prepare work items
2337
2476
  work_items = []
2338
2477
  for orig_label in unique_labels:
2339
- # find_objects returns list where index = label - 1
2340
2478
  bbox_index = orig_label - 1
2341
2479
 
2342
2480
  if (bbox_index >= 0 and
@@ -2346,52 +2484,94 @@ class ImageViewerWindow(QMainWindow):
2346
2484
  bbox = bounding_boxes[bbox_index]
2347
2485
  work_items.append((orig_label, bbox))
2348
2486
 
2349
- #print(f"Created {len(work_items)} work items")
2350
-
2351
- # If we have work items, process them
2352
2487
  if len(work_items) == 0:
2353
2488
  print("No valid work items found!")
2354
2489
  return np.zeros_like(input_array)
2355
2490
 
2356
- def process_label_minimal(item):
2357
- orig_label, bbox = item
2358
- try:
2359
- subarray = input_array[bbox]
2360
- binary_sub = subarray == orig_label
2361
-
2362
- if not np.any(binary_sub):
2363
- return orig_label, bbox, None, 0
2364
-
2365
- labeled_sub, num_cc = n3d.label_objects(binary_sub)
2366
- return orig_label, bbox, labeled_sub, num_cc
2367
-
2368
- except Exception as e:
2369
- #print(f"Error processing label {orig_label}: {e}")
2370
- return orig_label, bbox, None, 0
2371
-
2372
- # Execute in parallel
2491
+ # PASS 1: Split components, largest keeps label, others get new labels
2373
2492
  max_workers = min(mp.cpu_count(), len(work_items))
2374
2493
 
2375
2494
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
2376
- results = list(executor.map(process_label_minimal, work_items))
2495
+ process_func = lambda item: self.process_label_split_only(item, input_array)
2496
+ results = list(executor.map(process_func, work_items))
2377
2497
 
2378
- # Reconstruct output array
2379
- output_array = np.zeros_like(input_array)
2380
- current_label = max_val + 1
2381
- total_components = 0
2498
+ # Reconstruct output array from pass 1
2499
+ current_label = original_max_val + 1
2500
+ pass1_array = np.zeros_like(input_array)
2382
2501
 
2383
- for orig_label, bbox, labeled_sub, num_cc in results:
2502
+ for orig_label, bbox, labeled_sub, num_cc, largest_cc_id in results:
2384
2503
  if num_cc > 0 and labeled_sub is not None:
2385
- #print(f"Label {orig_label}: {num_cc} components")
2386
- # Remap labels and place in output
2387
2504
  for cc_id in range(1, num_cc + 1):
2388
2505
  mask = labeled_sub == cc_id
2389
- output_array[bbox][mask] = current_label
2390
- current_label += 1
2391
- total_components += 1
2506
+
2507
+ if cc_id == largest_cc_id:
2508
+ # Largest component keeps original label
2509
+ assigned_label = orig_label
2510
+ else:
2511
+ # Others get new incremental labels
2512
+ assigned_label = current_label
2513
+ current_label += 1
2514
+
2515
+ try:
2516
+ pass1_array[bbox][mask] = assigned_label
2517
+ except:
2518
+ # Handle dtype overflow
2519
+ if assigned_label < 256:
2520
+ dtype = np.uint8
2521
+ elif assigned_label < 65535:
2522
+ dtype = np.uint16
2523
+ else:
2524
+ dtype = np.uint32
2525
+ pass1_array = pass1_array.astype(dtype)
2526
+ pass1_array[bbox][mask] = assigned_label
2527
+
2528
+ print(f"Pass 1 complete. Created {current_label - original_max_val - 1} new labels")
2529
+
2530
+ # If branches=False, we're done
2531
+ if not branches:
2532
+ return pass1_array
2533
+
2534
+ # PASS 2 (branches=True): Reassign illegal labels based on legal neighbors
2535
+ print("Pass 2: Reassigning illegal labels based on legal neighbors")
2536
+
2537
+ # Find all labels that are > original_max_val (these are "illegal")
2538
+ illegal_mask = pass1_array > original_max_val
2539
+ if not np.any(illegal_mask):
2540
+ return pass1_array
2541
+
2542
+ illegal_labels = np.unique(pass1_array[illegal_mask])
2543
+ print(f"Processing {len(illegal_labels)} illegal labels")
2544
+
2545
+ # Get bounding boxes for illegal labels
2546
+ illegal_bboxes = ndimage.find_objects(pass1_array)
2392
2547
 
2393
- print(f"Total components created: {total_components}")
2394
- return output_array
2548
+ # Prepare work items for pass 2
2549
+ work_items_pass2 = []
2550
+ for illegal_label in illegal_labels:
2551
+ bbox_index = illegal_label - 1
2552
+
2553
+ if (bbox_index >= 0 and
2554
+ bbox_index < len(illegal_bboxes) and
2555
+ illegal_bboxes[bbox_index] is not None):
2556
+
2557
+ bbox = illegal_bboxes[bbox_index]
2558
+ work_items_pass2.append((illegal_label, bbox))
2559
+
2560
+ # Process illegal labels
2561
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
2562
+ process_func = lambda item: self.process_illegal_label_reassign(item, pass1_array, original_max_val)
2563
+ results_pass2 = list(executor.map(process_func, work_items_pass2))
2564
+
2565
+ # Apply pass 2 results
2566
+ pass2_array = pass1_array.copy()
2567
+
2568
+ for illegal_label, new_label in results_pass2:
2569
+ if new_label is not None and new_label != illegal_label:
2570
+ # Reassign this label
2571
+ pass2_array[pass1_array == illegal_label] = new_label
2572
+
2573
+ print(f"Pass 2 complete")
2574
+ return pass2_array
2395
2575
 
2396
2576
  def handle_seperate(self):
2397
2577
  """
@@ -2414,7 +2594,7 @@ class ImageViewerWindow(QMainWindow):
2414
2594
  non_highlighted = np.where(highlight_mask, 0, my_network.nodes)
2415
2595
 
2416
2596
  # Calculate max_val
2417
- max_val = np.max(non_highlighted) if np.any(non_highlighted) else 0
2597
+ max_val = np.max(self.channel_data[0])
2418
2598
 
2419
2599
  # Process highlighted part
2420
2600
  processed_highlights = self.separate_nontouching_objects(highlighted_nodes, max_val)
@@ -2438,7 +2618,7 @@ class ImageViewerWindow(QMainWindow):
2438
2618
  # Get non-highlighted part of the array
2439
2619
  non_highlighted = np.where(highlight_mask, 0, my_network.edges)
2440
2620
 
2441
- max_val = np.max(non_highlighted) if np.any(non_highlighted) else 0
2621
+ max_val = np.max(self.channel_data[1])
2442
2622
 
2443
2623
  # Process highlighted part
2444
2624
  processed_highlights = self.separate_nontouching_objects(highlighted_edges, max_val)
@@ -2625,6 +2805,21 @@ class ImageViewerWindow(QMainWindow):
2625
2805
 
2626
2806
  self.update_display()
2627
2807
 
2808
+ def toggle_scalebar(self):
2809
+
2810
+ try:
2811
+
2812
+ self.scale_bar = self.toggle_scale.isChecked()
2813
+ if self.scale_bar:
2814
+ self._draw_scalebar()
2815
+ self.update_display(preserve_zoom=(self.ax.get_xlim(), self.ax.get_ylim()))
2816
+ else:
2817
+ self._remove_scalebar()
2818
+ self.update_display(preserve_zoom=(self.ax.get_xlim(), self.ax.get_ylim()))
2819
+ except:
2820
+ self.scale_bar = False
2821
+ self.toggle_scale.setChecked(False)
2822
+ self._remove_scalebar()
2628
2823
 
2629
2824
 
2630
2825
  def toggle_highlight(self):
@@ -4191,10 +4386,8 @@ class ImageViewerWindow(QMainWindow):
4191
4386
  elif self.zoom_mode:
4192
4387
  # Handle zoom mode press
4193
4388
  if self.original_xlim is None:
4194
- self.original_xlim = self.ax.get_xlim()
4195
- #print(self.original_xlim)
4196
- self.original_ylim = self.ax.get_ylim()
4197
- #print(self.original_ylim)
4389
+ self.original_xlim = (-0.5, self.shape[2] - 0.5)
4390
+ self.original_ylim = (self.shape[1] + 0.5, -0.5)
4198
4391
 
4199
4392
  current_xlim = self.ax.get_xlim()
4200
4393
  current_ylim = self.ax.get_ylim()
@@ -4356,8 +4549,8 @@ class ImageViewerWindow(QMainWindow):
4356
4549
  if self.zoom_mode:
4357
4550
  # Existing zoom functionality
4358
4551
  if self.original_xlim is None:
4359
- self.original_xlim = self.ax.get_xlim()
4360
- self.original_ylim = self.ax.get_ylim()
4552
+ self.original_xlim = (-0.5, self.shape[2] - 0.5)
4553
+ self.original_ylim = (self.shape[1] + 0.5, -0.5)
4361
4554
 
4362
4555
  current_xlim = self.ax.get_xlim()
4363
4556
  current_ylim = self.ax.get_ylim()
@@ -4564,37 +4757,53 @@ class ImageViewerWindow(QMainWindow):
4564
4757
  report_action.triggered.connect(self.handle_report)
4565
4758
  partition_action = network_menu.addAction("Community Partition + Generic Community Stats")
4566
4759
  partition_action.triggered.connect(self.show_partition_dialog)
4567
- com_identity_action = network_menu.addAction("Identity Makeup of Network Communities (and UMAP)")
4760
+ com_identity_action = network_menu.addAction("Calculate Composition of Network Communities (and Show UMAP)")
4568
4761
  com_identity_action.triggered.connect(self.handle_com_id)
4569
4762
  com_neighbor_action = network_menu.addAction("Convert Network Communities into Neighborhoods? (Also Returns Compositional Heatmaps)")
4570
4763
  com_neighbor_action.triggered.connect(self.handle_com_neighbor)
4571
4764
  com_cell_action = network_menu.addAction("Create Communities Based on Cuboidal Proximity Cells?")
4572
4765
  com_cell_action.triggered.connect(self.handle_com_cell)
4766
+
4767
+
4573
4768
  stats_menu = analysis_menu.addMenu("Stats")
4574
- allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
4769
+ stats_net_menu = stats_menu.addMenu("Network Related")
4770
+ allstats_action = stats_net_menu.addAction("Calculate Generic Network Stats")
4575
4771
  allstats_action.triggered.connect(self.stats)
4576
- histos_action = stats_menu.addAction("Network Statistic Histograms")
4772
+ histos_action = stats_net_menu.addAction("Network Statistic Histograms")
4577
4773
  histos_action.triggered.connect(self.histos)
4578
- sig_action = stats_menu.addAction("Significance Testing")
4579
- sig_action.triggered.connect(self.sig_test)
4580
- radial_action = stats_menu.addAction("Radial Distribution Analysis")
4774
+ radial_action = stats_net_menu.addAction("Radial Distribution Analysis")
4581
4775
  radial_action.triggered.connect(self.show_radial_dialog)
4582
- neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
4776
+ heatmap_action = stats_net_menu.addAction("Community Cluster Heatmap")
4777
+ heatmap_action.triggered.connect(self.show_heatmap_dialog)
4778
+
4779
+ stats_space_menu = stats_menu.addMenu("Spatial")
4780
+ neighbor_id_action = stats_space_menu.addAction("Identity Distribution of Neighbors")
4583
4781
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
4584
- ripley_action = stats_menu.addAction("Ripley Clustering Analysis")
4782
+ ripley_action = stats_space_menu.addAction("Ripley Clustering Analysis")
4585
4783
  ripley_action.triggered.connect(self.show_ripley_dialog)
4586
- heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
4587
- heatmap_action.triggered.connect(self.show_heatmap_dialog)
4588
- nearneigh_action = stats_menu.addAction("Average Nearest Neighbors (With Clustering Heatmaps)")
4784
+ nearneigh_action = stats_space_menu.addAction("Average Nearest Neighbors (With Clustering Heatmaps)")
4589
4785
  nearneigh_action.triggered.connect(self.show_nearneigh_dialog)
4590
- vol_action = stats_menu.addAction("Calculate Volumes")
4786
+ inter_action = stats_space_menu.addAction("Calculate Node < > Edge Interaction")
4787
+ inter_action.triggered.connect(self.show_interaction_dialog)
4788
+
4789
+ stats_morph_menu = stats_menu.addMenu("Morphological")
4790
+ vol_action = stats_morph_menu.addAction("Calculate Volumes")
4591
4791
  vol_action.triggered.connect(self.volumes)
4592
- rad_action = stats_menu.addAction("Calculate Radii")
4792
+ rad_action = stats_morph_menu.addAction("Calculate Radii")
4593
4793
  rad_action.triggered.connect(self.show_rad_dialog)
4594
- inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
4595
- inter_action.triggered.connect(self.show_interaction_dialog)
4596
- violin_action = stats_menu.addAction("Show Identity Violins/UMAP")
4794
+ sa_action = stats_morph_menu.addAction("Calculate Surface Area")
4795
+ sa_action.triggered.connect(self.handle_sa)
4796
+ sphere_action = stats_morph_menu.addAction("Calculate Sphericities")
4797
+ sphere_action.triggered.connect(self.handle_sphericity)
4798
+ branch_stats = stats_morph_menu.addAction("Calculate Branch Stats (Lengths, Tortuosities)")
4799
+ branch_stats.triggered.connect(self.show_branchstat_dialog)
4800
+
4801
+ sig_action = stats_menu.addAction("Significance Testing")
4802
+ sig_action.triggered.connect(self.sig_test)
4803
+ violin_action = stats_menu.addAction("Show Identity Violins/UMAP/Assign Intensity Neighborhoods")
4597
4804
  violin_action.triggered.connect(self.show_violin_dialog)
4805
+
4806
+
4598
4807
  overlay_menu = analysis_menu.addMenu("Data/Overlays")
4599
4808
  degree_action = overlay_menu.addAction("Get Degree Information")
4600
4809
  degree_action.triggered.connect(self.show_degree_dialog)
@@ -4628,12 +4837,16 @@ class ImageViewerWindow(QMainWindow):
4628
4837
  calc_branch_action.triggered.connect(self.handle_calc_branch)
4629
4838
  calc_branchprox_action = calculate_menu.addAction("Calculate Branch Adjacency Network (Of Edges)")
4630
4839
  calc_branchprox_action.triggered.connect(self.handle_branchprox_calc)
4840
+ #calc_id_net_action = calculate_menu.addAction("Calculate Identity Network (beta)")
4841
+ #calc_id_net_action.triggered.connect(self.handle_identity_net_calc)
4631
4842
  centroid_action = calculate_menu.addAction("Calculate Centroids (Active Image)")
4632
4843
  centroid_action.triggered.connect(self.show_centroid_dialog)
4633
4844
 
4634
4845
  image_menu = process_menu.addMenu("Image")
4635
4846
  resize_action = image_menu.addAction("Resize (Up/Downsample)")
4636
4847
  resize_action.triggered.connect(self.show_resize_dialog)
4848
+ clean_action = image_menu.addAction("Clean Segmentation")
4849
+ clean_action.triggered.connect(self.show_clean_dialog)
4637
4850
  dilate_action = image_menu.addAction("Dilate")
4638
4851
  dilate_action.triggered.connect(self.show_dilate_dialog)
4639
4852
  erode_action = image_menu.addAction("Erode")
@@ -4644,7 +4857,7 @@ class ImageViewerWindow(QMainWindow):
4644
4857
  binarize_action.triggered.connect(self.show_binarize_dialog)
4645
4858
  label_action = image_menu.addAction("Label Objects")
4646
4859
  label_action.triggered.connect(self.show_label_dialog)
4647
- slabel_action = image_menu.addAction("Neighborhood Labels")
4860
+ slabel_action = image_menu.addAction("Neighbor Labels")
4648
4861
  slabel_action.triggered.connect(self.show_slabel_dialog)
4649
4862
  thresh_action = image_menu.addAction("Threshold/Segment")
4650
4863
  thresh_action.triggered.connect(self.show_thresh_dialog)
@@ -4674,6 +4887,8 @@ class ImageViewerWindow(QMainWindow):
4674
4887
  gennodes_action.triggered.connect(self.show_gennodes_dialog)
4675
4888
  branch_action = generate_menu.addAction("Label Branches")
4676
4889
  branch_action.triggered.connect(lambda: self.show_branch_dialog())
4890
+ filament_action = generate_menu.addAction("Trace Filaments (For Segmented Data)")
4891
+ filament_action.triggered.connect(self.show_filament_dialog)
4677
4892
  genvor_action = generate_menu.addAction("Generate Voronoi Diagram - goes in Overlay2")
4678
4893
  genvor_action.triggered.connect(self.voronoi)
4679
4894
 
@@ -4707,8 +4922,11 @@ class ImageViewerWindow(QMainWindow):
4707
4922
 
4708
4923
  # Help
4709
4924
 
4710
- help_button = menubar.addAction("Help")
4711
- help_button.triggered.connect(self.help_me)
4925
+ help_menu = menubar.addMenu("Help")
4926
+ documentation_action = help_menu.addAction("Documentation")
4927
+ documentation_action.triggered.connect(self.help_me)
4928
+ tutorial_action = help_menu.addAction("Tutorial")
4929
+ tutorial_action.triggered.connect(self.start_tutorial)
4712
4930
 
4713
4931
  # Initialize downsample factor
4714
4932
  self.downsample_factor = 1
@@ -4730,17 +4948,17 @@ class ImageViewerWindow(QMainWindow):
4730
4948
  corner_layout.addSpacing(10)
4731
4949
 
4732
4950
  # Add camera button
4733
- cam_button = QPushButton("📷")
4734
- cam_button.setFixedSize(40, 40)
4735
- cam_button.setStyleSheet("font-size: 24px;")
4736
- cam_button.clicked.connect(self.snap)
4737
- corner_layout.addWidget(cam_button)
4738
-
4739
- load_button = QPushButton("📁")
4740
- load_button.setFixedSize(40, 40)
4741
- load_button.setStyleSheet("font-size: 24px;")
4742
- load_button.clicked.connect(self.load_file)
4743
- corner_layout.addWidget(load_button)
4951
+ self.cam_button = QPushButton("📷")
4952
+ self.cam_button.setFixedSize(40, 40)
4953
+ self.cam_button.setStyleSheet("font-size: 24px;")
4954
+ self.cam_button.clicked.connect(self.snap)
4955
+ corner_layout.addWidget(self.cam_button)
4956
+
4957
+ self.load_button = QPushButton("📁")
4958
+ self.load_button.setFixedSize(40, 40)
4959
+ self.load_button.setStyleSheet("font-size: 24px;")
4960
+ self.load_button.clicked.connect(self.load_file)
4961
+ corner_layout.addWidget(self.load_button)
4744
4962
 
4745
4963
  # Set as corner widget
4746
4964
  menubar.setCornerWidget(corner_widget, Qt.Corner.TopRightCorner)
@@ -4831,13 +5049,13 @@ class ImageViewerWindow(QMainWindow):
4831
5049
  format_type = 'png'
4832
5050
 
4833
5051
  if self.downsample_factor > 1:
4834
- self.pan_mode = True # Update display will ignore downsamples if this is true so we can just use it here
5052
+ self.pan_mode = True
4835
5053
  self.downsample_factor = 1
4836
5054
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
4837
5055
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
4838
5056
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
4839
-
4840
- # Save with axes bbox
5057
+
5058
+ # Save with axes bbox (scalebar will be included if it's already drawn)
4841
5059
  bbox = self.ax.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
4842
5060
  self.figure.savefig(filename,
4843
5061
  dpi=300,
@@ -4848,12 +5066,89 @@ class ImageViewerWindow(QMainWindow):
4848
5066
  pad_inches=0)
4849
5067
 
4850
5068
  print(f"Axes snapshot saved: {filename}")
4851
-
4852
- self.toggle_pan_mode() # Assesses pan state since we messed with its vars potentially
5069
+ self.toggle_pan_mode()
4853
5070
 
4854
5071
  except Exception as e:
4855
5072
  print(f"Error saving snapshot: {e}")
4856
5073
 
5074
+ def _remove_scalebar(self):
5075
+ """Remove existing scalebar artists if present."""
5076
+ if hasattr(self, 'scalebar_artists') and self.scalebar_artists:
5077
+ for artist in self.scalebar_artists:
5078
+ artist.remove()
5079
+ self.scalebar_artists = None
5080
+
5081
+ def _draw_scalebar(self):
5082
+ """Draw a scale bar and store artists in self.scalebar_artists."""
5083
+ # Remove any existing scalebar first
5084
+ self._remove_scalebar()
5085
+ # Initialize the list
5086
+ self.scalebar_artists = []
5087
+ # Get current axis limits (in pixel coordinates)
5088
+ xlim = self.ax.get_xlim()
5089
+ ylim = self.ax.get_ylim()
5090
+ # Calculate view dimensions
5091
+ width_pixels = abs(xlim[1] - xlim[0])
5092
+ height_pixels = abs(ylim[1] - ylim[0])
5093
+ # Check if image is large enough for scale bar (minimum 200x100 pixels)
5094
+ #if width_pixels < 200 or height_pixels < 100:
5095
+ # return
5096
+ # Convert to actual units using xy_scale
5097
+ width_units = width_pixels * my_network.xy_scale
5098
+ # Determine a nice scale bar size (target ~15% of width)
5099
+ target_size = width_units * 0.15
5100
+ # Round to a nice number (1, 2, 5, 10, 20, 50, 100, etc.)
5101
+ magnitude = 10 ** np.floor(np.log10(target_size))
5102
+ normalized = target_size / magnitude
5103
+ if normalized < 1.5:
5104
+ nice_size = 1 * magnitude
5105
+ elif normalized < 3.5:
5106
+ nice_size = 2 * magnitude
5107
+ elif normalized < 7.5:
5108
+ nice_size = 5 * magnitude
5109
+ else:
5110
+ nice_size = 10 * magnitude
5111
+ # Convert back to pixels for drawing
5112
+ bar_length_pixels = nice_size / my_network.xy_scale
5113
+ # Position in bottom right corner with padding (5% margins)
5114
+ padding_x = width_pixels * 0.05
5115
+ padding_y = height_pixels * 0.05
5116
+ # Handle both normal and inverted y-axes (images typically use inverted y)
5117
+ x_max = max(xlim)
5118
+ y_bottom = max(ylim)
5119
+ bar_x_start = x_max - padding_x - bar_length_pixels
5120
+ bar_x_end = x_max - padding_x
5121
+ bar_y = y_bottom - padding_y
5122
+ # Draw scale bar with outline for visibility on any background
5123
+ # Black outline
5124
+ outline = self.ax.plot([bar_x_start, bar_x_end], [bar_y, bar_y],
5125
+ color='black', linewidth=6, solid_capstyle='butt',
5126
+ zorder=999)[0]
5127
+ # White main line
5128
+ line = self.ax.plot([bar_x_start, bar_x_end], [bar_y, bar_y],
5129
+ color='white', linewidth=4, solid_capstyle='butt',
5130
+ zorder=1000)[0]
5131
+ # Format the label text
5132
+ if nice_size >= 1:
5133
+ label_text = f'{nice_size:.0f}'
5134
+ else:
5135
+ label_text = f'{nice_size:.2f}'
5136
+ # Add text label above the bar (in visual space)
5137
+ # Place text above bar - offset depends on y-axis direction
5138
+ text_offset = height_pixels * 0.03 # Increased offset for better visibility
5139
+ if ylim[0] > ylim[1]: # Inverted y-axis (typical for images)
5140
+ text_y = bar_y - text_offset # Subtract to go visually up
5141
+ else: # Normal y-axis
5142
+ text_y = bar_y + text_offset # Add to go visually up
5143
+
5144
+ text_x = (bar_x_start + bar_x_end) / 2
5145
+ text = self.ax.text(text_x, text_y, label_text,
5146
+ color='white', fontsize=10, ha='center', va='bottom',
5147
+ fontweight='bold', zorder=1001,
5148
+ bbox=dict(boxstyle='round,pad=0.3', facecolor='black',
5149
+ edgecolor='none', alpha=0.7))
5150
+ # Store all artists
5151
+ self.scalebar_artists = [outline, line, text]
4857
5152
 
4858
5153
  def open_cellpose(self):
4859
5154
 
@@ -4876,8 +5171,11 @@ class ImageViewerWindow(QMainWindow):
4876
5171
  self.cellpose_launcher.launch_cellpose_gui(use_3d = use_3d)
4877
5172
 
4878
5173
  except:
4879
- import traceback
4880
- print(traceback.format_exc())
5174
+ QMessageBox.critical(
5175
+ self,
5176
+ "Error",
5177
+ f"Error starting cellpose: {str(e)}\nNote: You may need to install cellpose with corresponding torch first - in your environment, please call 'pip install cellpose'. Please see: 'https://pytorch.org/get-started/locally/' to see what torch install command corresponds to your NVIDIA GPU"
5178
+ )
4881
5179
  pass
4882
5180
 
4883
5181
 
@@ -4891,6 +5189,14 @@ class ImageViewerWindow(QMainWindow):
4891
5189
  print(f"Error opening URL: {e}")
4892
5190
  return False
4893
5191
 
5192
+ def start_tutorial(self):
5193
+ """Open the tutorial selection dialog"""
5194
+ if not hasattr(self, 'tutorial_dialog'):
5195
+ self.tutorial_dialog = TutorialSelectionDialog(self)
5196
+ self.tutorial_dialog.show()
5197
+ self.tutorial_dialog.raise_()
5198
+ self.tutorial_dialog.activateWindow()
5199
+
4894
5200
 
4895
5201
  def stats(self):
4896
5202
  """Method to get and display the network stats"""
@@ -4959,7 +5265,7 @@ class ImageViewerWindow(QMainWindow):
4959
5265
 
4960
5266
 
4961
5267
 
4962
- def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None, sort = True):
5268
+ def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None, sort = True, save = False):
4963
5269
  """
4964
5270
  Format dictionary or list data for display in upper right table.
4965
5271
 
@@ -5055,15 +5361,16 @@ class ImageViewerWindow(QMainWindow):
5055
5361
  # Add to tabbed widget
5056
5362
  if title is None:
5057
5363
  self.tabbed_data.add_table(f"{metric} Analysis", table)
5364
+ #print(list(self.tabbed_data.tables.values())[-1].model()._data)
5365
+ #for reference, the above is how you access the data in the tabbed data viz
5058
5366
  else:
5059
5367
  self.tabbed_data.add_table(f"{title}", table)
5060
-
5061
-
5062
-
5063
5368
  # Adjust column widths to content
5064
5369
  for column in range(table.model().columnCount(None)):
5065
5370
  table.resizeColumnToContents(column)
5066
5371
 
5372
+ if save:
5373
+ table.save_table_as('csv')
5067
5374
  return df
5068
5375
 
5069
5376
  except:
@@ -5114,12 +5421,15 @@ class ImageViewerWindow(QMainWindow):
5114
5421
  def show_calc_all_dialog(self):
5115
5422
  """Show the calculate all parameter dialog."""
5116
5423
  dialog = CalcAllDialog(self)
5117
- dialog.exec()
5424
+ dialog.show()
5118
5425
 
5119
- def show_calc_prox_dialog(self):
5426
+ def show_calc_prox_dialog(self, tutorial_example = False):
5120
5427
  """Show the proximity calc dialog"""
5121
- dialog = ProxDialog(self)
5122
- dialog.exec()
5428
+ dialog = ProxDialog(self, tutorial_example = True)
5429
+ if tutorial_example:
5430
+ dialog.show()
5431
+ else:
5432
+ dialog.exec()
5123
5433
 
5124
5434
  def table_load_attrs(self):
5125
5435
 
@@ -5182,8 +5492,6 @@ class ImageViewerWindow(QMainWindow):
5182
5492
  self.load_channel(1, my_network.nodes, data = True)
5183
5493
  self.delete_channel(0, False)
5184
5494
 
5185
- my_network.id_overlay = my_network.edges.copy()
5186
-
5187
5495
  self.show_gennodes_dialog()
5188
5496
 
5189
5497
  my_network.edges = (my_network.nodes == 0) * my_network.edges
@@ -5192,7 +5500,6 @@ class ImageViewerWindow(QMainWindow):
5192
5500
 
5193
5501
  self.load_channel(1, my_network.edges, data = True)
5194
5502
  self.load_channel(0, my_network.nodes, data = True)
5195
- self.load_channel(3, my_network.id_overlay, data = True)
5196
5503
 
5197
5504
  self.table_load_attrs()
5198
5505
 
@@ -5222,6 +5529,12 @@ class ImageViewerWindow(QMainWindow):
5222
5529
 
5223
5530
  self.load_channel(0, my_network.edges, data = True)
5224
5531
 
5532
+ try:
5533
+ self.branch_dict[0] = self.branch_dict[1]
5534
+ self.branch_dict[1] = None
5535
+ except:
5536
+ pass
5537
+
5225
5538
  self.delete_channel(1, False)
5226
5539
 
5227
5540
  my_network.morph_proximity(search = [3,3], fastdil = True)
@@ -5238,14 +5551,51 @@ class ImageViewerWindow(QMainWindow):
5238
5551
  dialog = CentroidDialog(self)
5239
5552
  dialog.exec()
5240
5553
 
5241
- def show_dilate_dialog(self):
5554
+ def handle_identity_net_calc(self):
5555
+
5556
+ try:
5557
+
5558
+ def confirm_dialog():
5559
+ """Shows a dialog asking user to confirm and input connection limit"""
5560
+ from PyQt6.QtWidgets import QInputDialog
5561
+
5562
+ value, ok = QInputDialog.getInt(
5563
+ None, # parent widget
5564
+ "Confirm", # window title
5565
+ "Calculate Identity Network\n\n"
5566
+ "Connect nodes that share an identity - useful for nodes that\n"
5567
+ "overlap in identity to some degree.\n\n"
5568
+ "Enter maximum connections per node within same identity:",
5569
+ 5, # default value
5570
+ 1, # minimum value
5571
+ 1000, # maximum value
5572
+ 1 # step
5573
+ )
5574
+
5575
+ if ok:
5576
+ return True, value
5577
+ else:
5578
+ return False, None
5579
+
5580
+ confirm, val = confirm_dialog()
5581
+
5582
+ if confirm:
5583
+ my_network.create_id_network(val)
5584
+ self.table_load_attrs()
5585
+ else:
5586
+ return
5587
+
5588
+ except:
5589
+ pass
5590
+
5591
+ def show_dilate_dialog(self, args = None):
5242
5592
  """show the dilate dialog"""
5243
- dialog = DilateDialog(self)
5244
- dialog.exec()
5593
+ dialog = DilateDialog(self, args)
5594
+ dialog.show()
5245
5595
 
5246
- def show_erode_dialog(self):
5596
+ def show_erode_dialog(self, args = None):
5247
5597
  """show the erode dialog"""
5248
- dialog = ErodeDialog(self)
5598
+ dialog = ErodeDialog(self, args)
5249
5599
  dialog.exec()
5250
5600
 
5251
5601
  def show_hole_dialog(self):
@@ -5253,6 +5603,11 @@ class ImageViewerWindow(QMainWindow):
5253
5603
  dialog = HoleDialog(self)
5254
5604
  dialog.exec()
5255
5605
 
5606
+ def show_filament_dialog(self):
5607
+ """show the filament dialog"""
5608
+ dialog = FilamentDialog(self)
5609
+ dialog.show()
5610
+
5256
5611
  def show_label_dialog(self):
5257
5612
  """Show the label dialog"""
5258
5613
  dialog = LabelDialog(self)
@@ -5263,13 +5618,20 @@ class ImageViewerWindow(QMainWindow):
5263
5618
  dialog = SLabelDialog(self)
5264
5619
  dialog.exec()
5265
5620
 
5266
- def show_thresh_dialog(self):
5621
+ def show_thresh_dialog(self, tutorial_example = False):
5267
5622
  """Show threshold dialog"""
5268
5623
  if self.machine_window is not None:
5269
5624
  return
5270
5625
 
5271
5626
  dialog = ThresholdDialog(self)
5272
- dialog.exec()
5627
+ if not tutorial_example:
5628
+ dialog.exec()
5629
+ else:
5630
+ dialog.show()
5631
+
5632
+ def show_machine_window_tutorial(self):
5633
+ dialog = MachineWindow(self, tutorial_example = True)
5634
+ dialog.show()
5273
5635
 
5274
5636
 
5275
5637
  def show_mask_dialog(self):
@@ -5306,15 +5668,21 @@ class ImageViewerWindow(QMainWindow):
5306
5668
  dialog.exec()
5307
5669
 
5308
5670
 
5309
- def show_gennodes_dialog(self, down_factor = None, called = False):
5671
+ def show_gennodes_dialog(self, down_factor = None, called = False, tutorial_example = False):
5310
5672
  """show the gennodes dialog"""
5311
5673
  gennodes = GenNodesDialog(self, down_factor = down_factor, called = called)
5312
- gennodes.exec()
5674
+ if not tutorial_example:
5675
+ gennodes.exec()
5676
+ else:
5677
+ gennodes.show()
5313
5678
 
5314
- def show_branch_dialog(self, called = False):
5679
+ def show_branch_dialog(self, called = False, tutorial_example = False):
5315
5680
  """Show the branch label dialog"""
5316
- dialog = BranchDialog(self, called = called)
5317
- dialog.exec()
5681
+ dialog = BranchDialog(self, called = called, tutorial_example = tutorial_example)
5682
+ if tutorial_example:
5683
+ dialog.show()
5684
+ else:
5685
+ dialog.exec()
5318
5686
 
5319
5687
  def voronoi(self):
5320
5688
 
@@ -5330,7 +5698,7 @@ class ImageViewerWindow(QMainWindow):
5330
5698
  def show_modify_dialog(self):
5331
5699
  """Show the network modify dialog"""
5332
5700
  dialog = ModifyDialog(self)
5333
- dialog.exec()
5701
+ dialog.show()
5334
5702
 
5335
5703
 
5336
5704
  def show_binarize_dialog(self):
@@ -5344,11 +5712,14 @@ class ImageViewerWindow(QMainWindow):
5344
5712
  dialog = ResizeDialog(self)
5345
5713
  dialog.exec()
5346
5714
 
5715
+ def show_clean_dialog(self):
5716
+ dialog = CleanDialog(self)
5717
+ dialog.show()
5347
5718
 
5348
5719
  def show_properties_dialog(self):
5349
5720
  """Show the properties dialog"""
5350
5721
  dialog = PropertiesDialog(self)
5351
- dialog.exec()
5722
+ dialog.show()
5352
5723
 
5353
5724
  def show_brightness_dialog(self):
5354
5725
  """Show the brightness/contrast control dialog."""
@@ -5960,7 +6331,7 @@ class ImageViewerWindow(QMainWindow):
5960
6331
  msg = QMessageBox()
5961
6332
  msg.setIcon(QMessageBox.Icon.Question)
5962
6333
  msg.setText("Image Format Alert")
5963
- msg.setInformativeText(f"This image is a different shape than the ones loaded into the viewer window. Trying to run processes with images of different sizes has a high probability of crashing the program.\nPress yes to resize the new image to the other images. Press no to load it anyway.")
6334
+ msg.setInformativeText(f"This image is a different shape than the ones loaded into the viewer window. This program is not designed to accomodate loading of differently sized images.\nPress yes to resize the new image to the other images. Press no to go back.")
5964
6335
  msg.setWindowTitle("Resize")
5965
6336
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
5966
6337
  return msg.exec() == QMessageBox.StandardButton.Yes
@@ -5987,11 +6358,13 @@ class ImageViewerWindow(QMainWindow):
5987
6358
  if 'YResolution' in tags:
5988
6359
  y_res = tags['YResolution'].value
5989
6360
  y_scale = y_res[1] / y_res[0] if isinstance(y_res, tuple) else 1.0 / y_res
5990
-
5991
- if x_scale is None:
6361
+
6362
+ if x_scale == None:
5992
6363
  x_scale = 1
5993
- if z_scale is None:
6364
+ if z_scale == None:
5994
6365
  z_scale = 1
6366
+ if x_scale == 1 and z_scale == 1:
6367
+ return
5995
6368
 
5996
6369
  return x_scale, z_scale
5997
6370
 
@@ -6026,7 +6399,11 @@ class ImageViewerWindow(QMainWindow):
6026
6399
  print(f"xy_scale property set to {my_network.xy_scale}; z_scale property set to {my_network.z_scale}")
6027
6400
  except:
6028
6401
  pass
6029
- self.channel_data[channel_index] = tifffile.imread(filename)
6402
+ test_channel_data = tifffile.imread(filename)
6403
+ if len(test_channel_data.shape) not in (2, 3, 4):
6404
+ print("Invalid Shape")
6405
+ return
6406
+ self.channel_data[channel_index] = test_channel_data
6030
6407
 
6031
6408
  elif file_extension == 'nii':
6032
6409
  import nibabel as nib
@@ -6104,7 +6481,6 @@ class ImageViewerWindow(QMainWindow):
6104
6481
  if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
6105
6482
  try:
6106
6483
  if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
6107
- self.resizing = True
6108
6484
  reset_resize = True
6109
6485
  self.highlight_overlay = None
6110
6486
  except:
@@ -6119,6 +6495,8 @@ class ImageViewerWindow(QMainWindow):
6119
6495
  if self.confirm_resize_dialog():
6120
6496
  self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
6121
6497
  break
6498
+ else:
6499
+ return
6122
6500
 
6123
6501
  if not begin_paint:
6124
6502
  if channel_index == 0:
@@ -6191,7 +6569,13 @@ class ImageViewerWindow(QMainWindow):
6191
6569
 
6192
6570
  if self.shape == self.channel_data[channel_index].shape:
6193
6571
  preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
6194
- self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
6572
+ self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
6573
+ else:
6574
+ if self.shape is not None:
6575
+ self.resizing = True
6576
+ self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
6577
+ ylim, xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
6578
+ preserve_zoom = (xlim, ylim)
6195
6579
  if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
6196
6580
  self.throttle = True
6197
6581
  else:
@@ -6199,7 +6583,6 @@ class ImageViewerWindow(QMainWindow):
6199
6583
 
6200
6584
 
6201
6585
  self.img_height, self.img_width = self.shape[1], self.shape[2]
6202
- self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
6203
6586
 
6204
6587
  self.completed_paint_strokes = [] #Reset pending paint operations
6205
6588
  self.current_stroke_points = []
@@ -6410,6 +6793,8 @@ class ImageViewerWindow(QMainWindow):
6410
6793
  #print(f"Saved {self.channel_names[ch_index]}" + (f" to: {filename}" if filename else "")) # Debug print
6411
6794
 
6412
6795
  except Exception as e:
6796
+ import traceback
6797
+ traceback.print_exc()
6413
6798
  QMessageBox.critical(
6414
6799
  self,
6415
6800
  "Error Saving File",
@@ -6433,12 +6818,9 @@ class ImageViewerWindow(QMainWindow):
6433
6818
  def update_slice(self):
6434
6819
  """Queue a slice update when slider moves."""
6435
6820
  # Store current view settings
6436
- if not self.resizing:
6437
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
6438
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
6439
- else:
6440
- current_xlim = None
6441
- current_ylim = None
6821
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
6822
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
6823
+
6442
6824
 
6443
6825
  # Store the pending slice and view settings
6444
6826
  self.pending_slice = (self.slice_slider.value(), (current_xlim, current_ylim))
@@ -6466,6 +6848,11 @@ class ImageViewerWindow(QMainWindow):
6466
6848
  self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
6467
6849
  elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
6468
6850
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
6851
+
6852
+ if self.resizing:
6853
+ self.highlight_overlay = None
6854
+ view_settings = ((-0.5, self.shape[2] - 0.5), (self.shape[1] - 0.5, -0.5))
6855
+ self.resizing = False
6469
6856
  self.update_display(preserve_zoom=view_settings)
6470
6857
  if self.pan_mode:
6471
6858
  self.pan_button.click()
@@ -6823,6 +7210,12 @@ class ImageViewerWindow(QMainWindow):
6823
7210
  if current_xlim is not None and current_ylim is not None:
6824
7211
  self.ax.set_xlim(current_xlim)
6825
7212
  self.ax.set_ylim(current_ylim)
7213
+
7214
+ if hasattr(self, 'scalebar_artists') and self.scalebar_artists:
7215
+ self._draw_scalebar()
7216
+ else:
7217
+ self._remove_scalebar()
7218
+
6826
7219
 
6827
7220
  if reset_resize:
6828
7221
  self.resizing = False
@@ -6833,8 +7226,6 @@ class ImageViewerWindow(QMainWindow):
6833
7226
 
6834
7227
  except Exception as e:
6835
7228
  pass
6836
- #import traceback
6837
- #print(traceback.format_exc())
6838
7229
 
6839
7230
 
6840
7231
  def get_channel_image(self, channel):
@@ -6944,12 +7335,73 @@ class ImageViewerWindow(QMainWindow):
6944
7335
  dialog = RadDialog(self)
6945
7336
  dialog.exec()
6946
7337
 
7338
+ def handle_sa(self):
7339
+
7340
+ try:
7341
+
7342
+ if self.shape[0] == 1:
7343
+ print("The image is 2D and therefore does not have surface areas")
7344
+ return
7345
+
7346
+ surface_areas = n3d.get_surface_areas(self.channel_data[self.active_channel], xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
7347
+
7348
+ if self.active_channel == 0:
7349
+ self.surface_area_dict[0] = surface_areas
7350
+ elif self.active_channel == 1:
7351
+ self.surface_area_dict[1] = surface_areas
7352
+ elif self.active_channel == 2:
7353
+ self.surface_area_dict[2] = surface_areas
7354
+ elif self.active_channel == 3:
7355
+ self.surface_area_dict[3] = surface_areas
7356
+
7357
+ self.format_for_upperright_table(surface_areas, title = '~Surface Areas of Objects (Jagged Faces)', metric='ObjectID', value='~Surface Area (Scaled)')
7358
+
7359
+ except Exception as e:
7360
+ print(f"Error: {e}")
7361
+
7362
+ def handle_sphericity(self):
7363
+
7364
+ try:
7365
+
7366
+ if self.shape[0] == 1:
7367
+ print("The image is 2D and therefore does not have sphericities")
7368
+ return
7369
+
7370
+ self.volumes()
7371
+ self.handle_sa()
7372
+ volumes = self.volume_dict[self.active_channel]
7373
+ surface_areas = self.surface_area_dict[self.active_channel]
7374
+
7375
+ sphericities = {
7376
+ label: (np.pi**(1/3) * (6 * volumes[label])**(2/3)) / surface_areas[label]
7377
+ for label in volumes.keys()
7378
+ if label in surface_areas and volumes[label] > 0 and surface_areas[label] > 0
7379
+ }
7380
+
7381
+ if self.active_channel == 0:
7382
+ self.sphericity_dict[0] = sphericities
7383
+ elif self.active_channel == 1:
7384
+ self.sphericity_dict[1] = sphericities
7385
+ elif self.active_channel == 2:
7386
+ self.sphericity_dict[2] = sphericities
7387
+ elif self.active_channel == 3:
7388
+ self.sphericity_dict[3] = sphericities
7389
+
7390
+ self.format_for_upperright_table(sphericities, title = 'Sphericities of Objects', metric='ObjectID', value='Sphericity')
7391
+
7392
+ except Exception as e:
7393
+ print(f"Error: {e}")
7394
+
7395
+ def show_branchstat_dialog(self):
7396
+ dialog = BranchStatDialog(self)
7397
+ dialog.exec()
7398
+
6947
7399
  def show_interaction_dialog(self):
6948
7400
  dialog = InteractionDialog(self)
6949
7401
  dialog.exec()
6950
7402
 
6951
- def show_violin_dialog(self):
6952
- dialog = ViolinDialog(self)
7403
+ def show_violin_dialog(self, called = False):
7404
+ dialog = ViolinDialog(self, called = called)
6953
7405
  dialog.show()
6954
7406
 
6955
7407
  def show_degree_dialog(self):
@@ -7156,7 +7608,7 @@ class CustomTableView(QTableView):
7156
7608
  else: # Bottom tables
7157
7609
  # Add Find action
7158
7610
  find_menu = context_menu.addMenu("Find")
7159
- find_action = find_menu.addAction("Find Node/Edge")
7611
+ find_action = find_menu.addAction("Find Node/Edge/")
7160
7612
  find_pair_action = find_menu.addAction("Find Pair")
7161
7613
  find_action.triggered.connect(lambda: self.handle_find_action(
7162
7614
  index.row(), index.column(),
@@ -7749,7 +8201,7 @@ class PropertiesDialog(QDialog):
7749
8201
  def __init__(self, parent=None):
7750
8202
  super().__init__(parent)
7751
8203
  self.setWindowTitle("Properties")
7752
- self.setModal(True)
8204
+ self.setModal(False)
7753
8205
 
7754
8206
  layout = QFormLayout(self)
7755
8207
 
@@ -7802,9 +8254,9 @@ class PropertiesDialog(QDialog):
7802
8254
  run_button.clicked.connect(self.run_properties)
7803
8255
  layout.addWidget(run_button)
7804
8256
 
7805
- report_button = QPushButton("Report Properties (Show in Top Right Tables)")
7806
- report_button.clicked.connect(self.report)
7807
- layout.addWidget(report_button)
8257
+ self.report_button = QPushButton("Report Properties (Show in Top Right Tables)")
8258
+ self.report_button.clicked.connect(self.report)
8259
+ layout.addWidget(self.report_button)
7808
8260
 
7809
8261
  def check_checked(self, ques):
7810
8262
 
@@ -8285,11 +8737,6 @@ class MergeNodeIdDialog(QDialog):
8285
8737
  self.mode_selector.addItems(["Auto-Binarize(Otsu)/Presegmented", "Manual (Interactive Thresholder)"])
8286
8738
  self.mode_selector.setCurrentIndex(1) # Default to Mode 1
8287
8739
  layout.addRow("Binarization Strategy:", self.mode_selector)
8288
-
8289
- self.umap = QPushButton("If using manual threshold: Also generate identity UMAP?")
8290
- self.umap.setCheckable(True)
8291
- self.umap.setChecked(True)
8292
- layout.addWidget(self.umap)
8293
8740
 
8294
8741
  self.include = QPushButton("Include When a Node is Negative for an ID?")
8295
8742
  self.include.setCheckable(True)
@@ -8346,7 +8793,7 @@ class MergeNodeIdDialog(QDialog):
8346
8793
  z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
8347
8794
  data = self.parent().channel_data[0]
8348
8795
  include = self.include.isChecked()
8349
- umap = self.umap.isChecked()
8796
+ umap = True
8350
8797
 
8351
8798
  if data is None:
8352
8799
  return
@@ -8481,18 +8928,23 @@ class MergeNodeIdDialog(QDialog):
8481
8928
  all_keys = id_dicts[0].keys()
8482
8929
  result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
8483
8930
 
8484
-
8485
- self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity (Save this Table for "Analyze -> Stats -> Show Violins")')
8486
- if umap:
8487
- my_network.identity_umap(result)
8488
-
8489
-
8490
8931
  QMessageBox.information(
8491
8932
  self,
8492
8933
  "Success",
8493
8934
  "Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. If desired, please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. If desired, please save the outputted mean intensity table to use with 'Analyze -> Stats -> Show Violins'. (Press Help [above] for more info)"
8494
8935
  )
8495
8936
 
8937
+ print("Please save your identity table if desired for use with the violin plot and intensity neighborhoods function")
8938
+ self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity (Save this Table for "Analyze -> Stats -> Show Violins")', save = True)
8939
+ try:
8940
+ self.parent().show_violin_dialog(called = True)
8941
+ QMessageBox.information(
8942
+ self,
8943
+ "FYI",
8944
+ "Here is the violin plot/intensity neighborhoods function control window for the aforementioned table. Feel free to close these windows if you do not desire to use this analysis, however you will need to reference the saved table to get back here."
8945
+ )
8946
+ except:
8947
+ pass
8496
8948
  self.accept()
8497
8949
  else:
8498
8950
  my_network.merge_node_ids(selected_path, data, include)
@@ -8611,7 +9063,7 @@ class Show3dDialog(QDialog):
8611
9063
  self.cubic = QPushButton("Cubic")
8612
9064
  self.cubic.setCheckable(True)
8613
9065
  self.cubic.setChecked(False)
8614
- layout.addRow("Use cubic downsample (Slower but preserves shape better potentially)?", self.cubic)
9066
+ layout.addRow("Use cubic downsample (Slower but preserves visualization better potentially)?", self.cubic)
8615
9067
 
8616
9068
  self.box = QPushButton("Box")
8617
9069
  self.box.setCheckable(True)
@@ -8662,6 +9114,9 @@ class Show3dDialog(QDialog):
8662
9114
  if visible:
8663
9115
  arrays_4d.append(channel)
8664
9116
 
9117
+ if self.parent().thresh_window_ref is not None:
9118
+ self.parent().thresh_window_ref.make_full_highlight()
9119
+
8665
9120
  if self.parent().highlight_overlay is not None or self.parent().mini_overlay_data is not None:
8666
9121
  if self.parent().mini_overlay == True:
8667
9122
  self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
@@ -8673,6 +9128,11 @@ class Show3dDialog(QDialog):
8673
9128
  self.accept()
8674
9129
 
8675
9130
  except Exception as e:
9131
+ QMessageBox.critical(
9132
+ self,
9133
+ "Error",
9134
+ f"Error showing 3D: {str(e)}\nNote: You may need to install napari first - in your environment, please call 'pip install napari'"
9135
+ )
8676
9136
  print(f"Error: {e}")
8677
9137
  import traceback
8678
9138
  print(traceback.format_exc())
@@ -8688,6 +9148,9 @@ class NetOverlayDialog(QDialog):
8688
9148
 
8689
9149
  layout = QFormLayout(self)
8690
9150
 
9151
+ self.downsample = QLineEdit("")
9152
+ layout.addRow("Downsample Factor While Drawing? (Int - Makes the outputted lines larger):", self.downsample)
9153
+
8691
9154
  # Add Run button
8692
9155
  run_button = QPushButton("Generate (Will go to Overlay 1)")
8693
9156
  run_button.clicked.connect(self.netoverlay)
@@ -8704,7 +9167,16 @@ class NetOverlayDialog(QDialog):
8704
9167
  if my_network.node_centroids is None:
8705
9168
  return
8706
9169
 
8707
- my_network.network_overlay = my_network.draw_network()
9170
+ try:
9171
+ downsample = float(self.downsample.text()) if self.downsample.text() else None
9172
+ except ValueError:
9173
+ downsample = None
9174
+
9175
+ my_network.network_overlay = my_network.draw_network(down_factor = downsample)
9176
+
9177
+ if downsample is not None:
9178
+ my_network.network_overlay = n3d.upsample_with_padding(my_network.network_overlay, original_shape = self.parent().shape)
9179
+
8708
9180
 
8709
9181
  self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
8710
9182
 
@@ -8731,6 +9203,9 @@ class IdOverlayDialog(QDialog):
8731
9203
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
8732
9204
  layout.addRow("Execution Mode:", self.mode_selector)
8733
9205
 
9206
+ self.downsample = QLineEdit("")
9207
+ layout.addRow("Downsample Factor While Drawing? (Int - Makes the outputted numbers larger):", self.downsample)
9208
+
8734
9209
  # Add Run button
8735
9210
  run_button = QPushButton("Generate (Will go to Overlay 2)")
8736
9211
  run_button.clicked.connect(self.idoverlay)
@@ -8738,38 +9213,51 @@ class IdOverlayDialog(QDialog):
8738
9213
 
8739
9214
  def idoverlay(self):
8740
9215
 
8741
- accepted_mode = self.mode_selector.currentIndex()
9216
+ try:
8742
9217
 
8743
- if accepted_mode == 0:
9218
+ accepted_mode = self.mode_selector.currentIndex()
8744
9219
 
8745
- if my_network.node_centroids is None:
9220
+ try:
9221
+ downsample = float(self.downsample.text()) if self.downsample.text() else None
9222
+ except ValueError:
9223
+ downsample = None
8746
9224
 
8747
- self.parent().show_centroid_dialog()
9225
+ if accepted_mode == 0:
8748
9226
 
8749
- if my_network.node_centroids is None:
8750
- return
9227
+ if my_network.node_centroids is None:
8751
9228
 
8752
- elif accepted_mode == 1:
9229
+ self.parent().show_centroid_dialog()
8753
9230
 
8754
- if my_network.edge_centroids is None:
9231
+ if my_network.node_centroids is None:
9232
+ return
8755
9233
 
8756
- self.parent().show_centroid_dialog()
9234
+ elif accepted_mode == 1:
8757
9235
 
8758
- if my_network.edge_centroids is None:
8759
- return
9236
+ if my_network.edge_centroids is None:
9237
+
9238
+ self.parent().show_centroid_dialog()
8760
9239
 
8761
- if accepted_mode == 0:
9240
+ if my_network.edge_centroids is None:
9241
+ return
8762
9242
 
8763
- my_network.id_overlay = my_network.draw_node_indices()
9243
+ if accepted_mode == 0:
8764
9244
 
8765
- elif accepted_mode == 1:
9245
+ my_network.id_overlay = my_network.draw_node_indices(down_factor = downsample)
8766
9246
 
8767
- my_network.id_overlay = my_network.draw_edge_indices()
9247
+ elif accepted_mode == 1:
8768
9248
 
9249
+ my_network.id_overlay = my_network.draw_edge_indices(down_factor = downsample)
8769
9250
 
8770
- self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9251
+ if downsample is not None:
9252
+ my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
9253
+
9254
+ self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9255
+
9256
+ self.accept()
9257
+
9258
+ except:
9259
+ print(f"Error with Overlay Generation: {e}")
8771
9260
 
8772
- self.accept()
8773
9261
 
8774
9262
  class ColorOverlayDialog(QDialog):
8775
9263
 
@@ -8791,7 +9279,7 @@ class ColorOverlayDialog(QDialog):
8791
9279
  layout.addRow("Execution Mode:", self.mode_selector)
8792
9280
 
8793
9281
  self.down_factor = QLineEdit("")
8794
- layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
9282
+ layout.addRow("down_factor (int - for speeding up overlay generation - optional):", self.down_factor)
8795
9283
 
8796
9284
  # Add Run button
8797
9285
  run_button = QPushButton("Generate (Will go to Overlay 2)")
@@ -8945,11 +9433,6 @@ class NetShowDialog(QDialog):
8945
9433
  self.weighted.setCheckable(True)
8946
9434
  self.weighted.setChecked(True)
8947
9435
  layout.addRow("Use Weighted Network (Only for community graphs):", self.weighted)
8948
-
8949
- # Optional saving:
8950
- self.directory = QLineEdit()
8951
- self.directory.setPlaceholderText("Does not save when empty")
8952
- layout.addRow("Output Directory:", self.directory)
8953
9436
 
8954
9437
  # Add Run button
8955
9438
  run_button = QPushButton("Show Network")
@@ -8965,7 +9448,7 @@ class NetShowDialog(QDialog):
8965
9448
  self.parent().show_centroid_dialog()
8966
9449
  accepted_mode = self.mode_selector.currentIndex() # Convert to 1-based index
8967
9450
  # Get directory (None if empty)
8968
- directory = self.directory.text() if self.directory.text() else None
9451
+ directory = None
8969
9452
 
8970
9453
  weighted = self.weighted.isChecked()
8971
9454
 
@@ -9008,7 +9491,7 @@ class PartitionDialog(QDialog):
9008
9491
 
9009
9492
  # Add mode selection dropdown
9010
9493
  self.mode_selector = QComboBox()
9011
- self.mode_selector.addItems(["Label Propogation", "Louvain"])
9494
+ self.mode_selector.addItems(["Louvain", "Label Propogation"])
9012
9495
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
9013
9496
  layout.addRow("Execution Mode:", self.mode_selector)
9014
9497
 
@@ -9031,6 +9514,10 @@ class PartitionDialog(QDialog):
9031
9514
  self.parent().prev_coms = None
9032
9515
 
9033
9516
  accepted_mode = self.mode_selector.currentIndex()
9517
+ if accepted_mode == 0: #I switched where these are in the selection box
9518
+ accepted_mode = 1
9519
+ elif accepted_mode == 1:
9520
+ accepted_mode = 0
9034
9521
  weighted = self.weighted.isChecked()
9035
9522
  dostats = self.stats.isChecked()
9036
9523
 
@@ -9307,9 +9794,6 @@ class RadialDialog(QDialog):
9307
9794
  self.distance = QLineEdit("50")
9308
9795
  layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
9309
9796
 
9310
- self.directory = QLineEdit("")
9311
- layout.addRow("Output Directory:", self.directory)
9312
-
9313
9797
  # Add Run button
9314
9798
  run_button = QPushButton("Get Radial Distribution")
9315
9799
  run_button.clicked.connect(self.radial)
@@ -9321,7 +9805,7 @@ class RadialDialog(QDialog):
9321
9805
 
9322
9806
  distance = float(self.distance.text()) if self.distance.text().strip() else 50
9323
9807
 
9324
- directory = str(self.distance.text()) if self.directory.text().strip() else None
9808
+ directory = None
9325
9809
 
9326
9810
  if my_network.node_centroids is None:
9327
9811
  self.parent().show_centroid_dialog()
@@ -9605,9 +10089,6 @@ class NeighborIdentityDialog(QDialog):
9605
10089
  else:
9606
10090
  self.root = None
9607
10091
 
9608
- self.directory = QLineEdit("")
9609
- layout.addRow("Output Directory:", self.directory)
9610
-
9611
10092
  self.mode = QComboBox()
9612
10093
  self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
9613
10094
  self.mode.setCurrentIndex(0)
@@ -9619,7 +10100,7 @@ class NeighborIdentityDialog(QDialog):
9619
10100
  self.fastdil = QPushButton("Fast Dilate")
9620
10101
  self.fastdil.setCheckable(True)
9621
10102
  self.fastdil.setChecked(False)
9622
- layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10103
+ #layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
9623
10104
 
9624
10105
  # Add Run button
9625
10106
  run_button = QPushButton("Get Neighborhood Identity Distribution")
@@ -9635,7 +10116,7 @@ class NeighborIdentityDialog(QDialog):
9635
10116
  except:
9636
10117
  pass
9637
10118
 
9638
- directory = self.directory.text() if self.directory.text().strip() else None
10119
+ directory = None
9639
10120
 
9640
10121
  mode = self.mode.currentIndex()
9641
10122
 
@@ -10030,7 +10511,7 @@ class RadDialog(QDialog):
10030
10511
  self.GPU = QPushButton("GPU")
10031
10512
  self.GPU.setCheckable(True)
10032
10513
  self.GPU.setChecked(False)
10033
- layout.addRow("Use GPU:", self.GPU)
10514
+ #layout.addRow("Use GPU:", self.GPU)
10034
10515
 
10035
10516
 
10036
10517
  # Add Run button
@@ -10064,9 +10545,6 @@ class RadDialog(QDialog):
10064
10545
  print(f"Error: {e}")
10065
10546
 
10066
10547
 
10067
-
10068
-
10069
-
10070
10548
  class InteractionDialog(QDialog):
10071
10549
 
10072
10550
  def __init__(self, parent=None):
@@ -10108,7 +10586,7 @@ class InteractionDialog(QDialog):
10108
10586
  self.fastdil = QPushButton("Fast Dilate")
10109
10587
  self.fastdil.setCheckable(True)
10110
10588
  self.fastdil.setChecked(False)
10111
- layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10589
+ #layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10112
10590
 
10113
10591
  # Add Run button
10114
10592
  run_button = QPushButton("Calculate")
@@ -10151,34 +10629,40 @@ class InteractionDialog(QDialog):
10151
10629
 
10152
10630
  class ViolinDialog(QDialog):
10153
10631
 
10154
- def __init__(self, parent=None):
10155
-
10632
+ def __init__(self, parent=None, called = False):
10156
10633
  super().__init__(parent)
10157
-
10158
- QMessageBox.critical(
10159
- self,
10160
- "Notice",
10161
- "Please select spreadsheet (Should be table output of 'File -> Images -> Node Identities -> Assign Node Identities from Overlap with Other Images'. Make sure to save that table as .csv/.xlsx and then load it here to use this.)"
10162
- )
10163
-
10634
+ if not called:
10635
+ QMessageBox.critical(
10636
+ self,
10637
+ "Notice",
10638
+ "Please select spreadsheet (Should be table output of 'File -> Images -> Node Identities -> Assign Node Identities from Overlap with Other Images'. Make sure to save that table as .csv/.xlsx and then load it here to use this.)"
10639
+ )
10164
10640
  try:
10641
+ if not called:
10642
+ try:
10643
+ self.df = self.parent().load_file()
10644
+ except:
10645
+ return
10646
+ else:
10647
+ try:
10648
+ self.df = list(self.parent().tabbed_data.tables.values())[-1].model()._data
10649
+ except:
10650
+ pass
10651
+ try:
10652
+ self.backup_df = copy.deepcopy(self.df)
10653
+ except:
10654
+ pass
10165
10655
  try:
10166
- self.df = self.parent().load_file()
10656
+ # Get all identity lists and normalize the dataframe
10657
+ identity_lists = self.get_all_identity_lists()
10658
+ self.df = self.normalize_df_with_identity_centerpoints(self.df, identity_lists)
10167
10659
  except:
10168
- return
10169
-
10170
- self.backup_df = copy.deepcopy(self.df)
10171
- # Get all identity lists and normalize the dataframe
10172
- identity_lists = self.get_all_identity_lists()
10173
- self.df = self.normalize_df_with_identity_centerpoints(self.df, identity_lists)
10174
-
10175
- self.setWindowTitle("Violin Parameters")
10660
+ pass
10661
+ self.setWindowTitle("Violin/Neighborhood Parameters")
10176
10662
  self.setModal(False)
10177
-
10178
10663
  layout = QFormLayout(self)
10179
-
10664
+
10180
10665
  if my_network.node_identities is not None:
10181
-
10182
10666
  self.idens = QComboBox()
10183
10667
  all_idens = list(set(my_network.node_identities.values()))
10184
10668
  idens = []
@@ -10200,16 +10684,49 @@ class ViolinDialog(QDialog):
10200
10684
  self.coms.addItems(coms)
10201
10685
  self.coms.setCurrentIndex(0)
10202
10686
  layout.addRow("Return Neighborhood/Community Violin Plots?", self.coms)
10203
-
10687
+
10204
10688
  # Add Run button
10205
10689
  run_button = QPushButton("Show Z-score-like Violin")
10206
10690
  run_button.clicked.connect(self.run)
10207
10691
  layout.addWidget(run_button)
10208
-
10692
+
10209
10693
  run_button2 = QPushButton("Show Z-score UMAP")
10210
10694
  run_button2.clicked.connect(self.run2)
10211
- layout.addWidget(run_button2)
10695
+ self.mode_selector = QComboBox()
10696
+ self.mode_selector.addItems(["Label UMAP By Identity", "Label UMAP By Neighborhood/Community"])
10697
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
10698
+ layout.addRow("Execution Mode:", self.mode_selector)
10699
+ layout.addRow(self.mode_selector, run_button2)
10700
+
10701
+ # Add separator to visually group the clustering options
10702
+ from PyQt6.QtWidgets import QFrame
10703
+ separator = QFrame()
10704
+ separator.setFrameShape(QFrame.Shape.HLine)
10705
+ separator.setFrameShadow(QFrame.Shadow.Sunken)
10706
+ layout.addRow(separator)
10707
+
10708
+ # Clustering options section (visually grouped)
10709
+ clustering_label = QLabel("<b>Clustering Options:</b>")
10710
+ layout.addRow(clustering_label)
10711
+
10712
+ # KMeans clustering
10713
+ run_button3 = QPushButton("Assign Neighborhoods via KMeans Clustering")
10714
+ run_button3.clicked.connect(self.run3)
10715
+ self.kmeans_num_input = QLineEdit()
10716
+ self.kmeans_num_input.setPlaceholderText("Auto (num neighborhoods)")
10717
+ self.kmeans_num_input.setMaximumWidth(150)
10718
+ from PyQt6.QtGui import QIntValidator
10719
+ self.kmeans_num_input.setValidator(QIntValidator(1, 1000))
10720
+ layout.addRow(run_button3, self.kmeans_num_input)
10721
+
10722
+ # Reassign identities checkbox
10723
+ self.reassign_identities_checkbox = QCheckBox("Reassign Identities Based on Clustering Results?")
10724
+ self.reassign_identities_checkbox.setChecked(False)
10725
+ layout.addRow(self.reassign_identities_checkbox)
10726
+
10212
10727
  except:
10728
+ import traceback
10729
+ print(traceback.format_exc())
10213
10730
  QTimer.singleShot(0, self.close)
10214
10731
 
10215
10732
  def get_all_identity_lists(self):
@@ -10251,6 +10768,63 @@ class ViolinDialog(QDialog):
10251
10768
 
10252
10769
  return identity_lists
10253
10770
 
10771
+ def prepare_data_for_umap(self, df, node_identities=None):
10772
+ """
10773
+ Prepare data for UMAP visualization by z-score normalizing columns.
10774
+
10775
+ Args:
10776
+ df: DataFrame with first column as NodeID, rest as marker intensities
10777
+ node_identities: Optional dict mapping node_id (int) -> identity (string).
10778
+ If provided, only nodes present as keys will be kept.
10779
+
10780
+ Returns:
10781
+ dict: {node_id: [normalized_marker_values]}
10782
+ """
10783
+ from sklearn.preprocessing import StandardScaler
10784
+ import numpy as np
10785
+
10786
+ # Store marker names (column headers) before converting to numpy array
10787
+ marker_names = df.columns[1:].tolist() # All columns except first (NodeID)
10788
+ node_id_col_name = df.columns[0] # Store the first column name (e.g., "NodeID")
10789
+
10790
+ # Extract node IDs from first column
10791
+ node_ids = df.iloc[:, 0].values
10792
+ # Extract marker data (all columns except first)
10793
+ X = df.iloc[:, 1:].values
10794
+
10795
+ # Z-score normalization (column-wise)
10796
+ scaler = StandardScaler() # Ultimately decided to normalize with the entirety of the available data (even cells without identities) since those cells' low expression should represent something of a ground truth of background expression which is relevant for normalizing.
10797
+ X_normalized = scaler.fit_transform(X)
10798
+
10799
+ # Filter if node_identities is provided
10800
+ if my_network.node_identities is not None: # And then after norm we can remove irrelevant cells as we don't random uninvolved cells to be considered in the grouping algorithms (ie umap and kmeans)
10801
+ # Get the valid node IDs from node_identities keys
10802
+ valid_node_ids = set(my_network.node_identities.keys())
10803
+
10804
+ # Create mask for valid node IDs
10805
+ mask = pd.Series(node_ids).isin(valid_node_ids).values
10806
+
10807
+ # Filter both node_ids and X_normalized using the mask
10808
+ node_ids = node_ids[mask]
10809
+ X_normalized = X_normalized[mask]
10810
+
10811
+ # Optional: Check if any rows remain after filtering
10812
+ if len(node_ids) == 0:
10813
+ raise ValueError("No matching nodes found between df and node_identities")
10814
+
10815
+ # Reconstruct DataFrame with normalized values
10816
+ self.ref_df = pd.DataFrame(X_normalized, columns=marker_names)
10817
+ self.ref_df.insert(0, node_id_col_name, node_ids) # Add NodeID column back as first column
10818
+
10819
+ # Create dictionary mapping node_id -> normalized row
10820
+ result_dict = {
10821
+ int(node_ids[i]): X_normalized[i].tolist()
10822
+ for i in range(len(node_ids))
10823
+ }
10824
+
10825
+ return result_dict
10826
+
10827
+
10254
10828
  def normalize_df_with_identity_centerpoints(self, df, identity_lists):
10255
10829
  """
10256
10830
  Normalize the entire dataframe using identity-specific centerpoints.
@@ -10282,7 +10856,7 @@ class ViolinDialog(QDialog):
10282
10856
  # Get nodes that exist in both the identity list and the dataframe
10283
10857
  valid_nodes = [node for node in node_list if node in df_copy.index]
10284
10858
  if valid_nodes and ((str(identity) == str(column)) or str(identity) == f'{str(column)}+'):
10285
- # Get the median value for this identity in this column
10859
+ # Get the min value for this identity in this column
10286
10860
  identity_min = df_copy.loc[valid_nodes, column].min()
10287
10861
  centerpoint = identity_min
10288
10862
  break # Found the match, no need to continue
@@ -10338,7 +10912,7 @@ class ViolinDialog(QDialog):
10338
10912
  for column in range(table.model().columnCount(None)):
10339
10913
  table.resizeColumnToContents(column)
10340
10914
 
10341
- def run(self):
10915
+ def run(self, com = None):
10342
10916
 
10343
10917
  def df_to_dict_by_rows(df, row_indices, title):
10344
10918
  """
@@ -10377,6 +10951,23 @@ class ViolinDialog(QDialog):
10377
10951
 
10378
10952
  from . import neighborhoods
10379
10953
 
10954
+ try:
10955
+ if com:
10956
+
10957
+ self.ref_df = self.df
10958
+
10959
+ com_dict = n3d.invert_dict(my_network.communities)
10960
+
10961
+ com_list = com_dict[int(com)]
10962
+
10963
+ violin_dict = df_to_dict_by_rows(self.df, com_list, f"Z-Score-like Channel Intensities of Community/Neighborhood {com}, {len(com_list)} Nodes")
10964
+
10965
+ neighborhoods.create_violin_plots(violin_dict, graph_title=f"Z-Score-like Channel Intensities of Community/Neighborhood {com}, {len(com_list)} Nodes")
10966
+
10967
+ return
10968
+ except:
10969
+ pass
10970
+
10380
10971
  try:
10381
10972
 
10382
10973
  if self.idens.currentIndex() != 0:
@@ -10416,31 +11007,166 @@ class ViolinDialog(QDialog):
10416
11007
  except:
10417
11008
  pass
10418
11009
 
10419
-
10420
11010
  def run2(self):
10421
- def df_to_dict(df):
10422
- # Make a copy to avoid modifying the original dataframe
10423
- df_copy = df.copy()
10424
-
10425
- # Set the first column as the index (row headers)
10426
- df_copy = df_copy.set_index(df_copy.columns[0])
10427
-
10428
- # Convert all remaining columns to float type (batch conversion)
10429
- df_copy = df_copy.astype(float)
10430
-
10431
- # Create the result dictionary
10432
- result_dict = {}
10433
- for row_idx in df_copy.index:
10434
- result_dict[row_idx] = df_copy.loc[row_idx].tolist()
10435
-
10436
- return result_dict
10437
11011
 
10438
11012
  try:
10439
- umap_dict = df_to_dict(self.backup_df)
10440
- my_network.identity_umap(umap_dict)
11013
+ umap_dict = self.prepare_data_for_umap(self.backup_df)
11014
+ mode = self.mode_selector.currentIndex()
11015
+ my_network.identity_umap(umap_dict, mode)
10441
11016
  except:
11017
+ import traceback
11018
+ print(traceback.format_exc())
10442
11019
  pass
10443
11020
 
11021
+ def run3(self):
11022
+ num_clusters_text = self.kmeans_num_input.text()
11023
+
11024
+ if num_clusters_text:
11025
+ num_clusters = int(num_clusters_text)
11026
+ # Use specified number of clusters
11027
+ print(f"Using {num_clusters} clusters")
11028
+ else:
11029
+ num_clusters = None # Auto-determine
11030
+ print("Auto-determining number of clusters")
11031
+ try:
11032
+ cluster_dict = self.prepare_data_for_umap(self.backup_df)
11033
+ my_network.group_nodes_by_intensity(cluster_dict, count = num_clusters)
11034
+
11035
+ try:
11036
+ # Check if user wants to reassign identities
11037
+ if self.reassign_identities_checkbox.isChecked():
11038
+ # Invert the dict to get {neighborhood_id: [node_ids]}
11039
+ inverted_dict = n3d.invert_dict(my_network.communities)
11040
+
11041
+ # Dictionary to store old -> new neighborhood names
11042
+ neighborhood_rename_dict = {}
11043
+ neighborhood_items = list(inverted_dict.items())
11044
+
11045
+ def show_next_dialog(index=0):
11046
+ if index >= len(neighborhood_items):
11047
+ temp_dict = copy.deepcopy(neighborhood_rename_dict)
11048
+ for item in temp_dict:
11049
+ if temp_dict[item] == None:
11050
+ del neighborhood_rename_dict[item]
11051
+ # All dialogs done, apply the renaming
11052
+ for node_id, old_neighborhood_id in my_network.communities.items():
11053
+ try:
11054
+ # Only update identity if this neighborhood was renamed
11055
+ if old_neighborhood_id in neighborhood_rename_dict:
11056
+ my_network.node_identities[node_id] = neighborhood_rename_dict[old_neighborhood_id]
11057
+ # Otherwise, keep the existing identity (do nothing)
11058
+ except:
11059
+ pass
11060
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', title = 'Node Communities')
11061
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', title = 'Node Identities')
11062
+ self.accept()
11063
+ return
11064
+
11065
+ neighborhood_id, node_list = neighborhood_items[index]
11066
+
11067
+ plt.close()
11068
+ self.run(com = neighborhood_id)
11069
+
11070
+ # Filter self.ref_df to only nodes in this neighborhood
11071
+ mask = self.ref_df.iloc[:, 0].isin(node_list)
11072
+ filtered_df = self.ref_df[mask]
11073
+
11074
+ # Calculate average for each marker (skip first column which is NodeID)
11075
+ averages = filtered_df.iloc[:, 1:].mean()
11076
+
11077
+ # Show dialog to user
11078
+ dialog = NeighborhoodRenameDialog(
11079
+ neighborhood_id=neighborhood_id,
11080
+ averages=averages,
11081
+ node_count=len(node_list),
11082
+ parent=self
11083
+ )
11084
+
11085
+ def on_dialog_finished(result):
11086
+ if result == QDialog.DialogCode.Accepted:
11087
+ new_name = dialog.get_new_name()
11088
+ if new_name: # If user provided a non-empty name
11089
+ neighborhood_rename_dict[neighborhood_id] = new_name
11090
+ else: # User clicked OK but left it blank
11091
+ neighborhood_rename_dict[neighborhood_id] = None
11092
+ else:
11093
+ # User cancelled or closed window
11094
+ neighborhood_rename_dict[neighborhood_id] = None
11095
+
11096
+ # Show next dialog
11097
+ show_next_dialog(index + 1)
11098
+
11099
+ dialog.finished.connect(on_dialog_finished)
11100
+ dialog.show()
11101
+
11102
+ # Start the chain
11103
+ show_next_dialog(0)
11104
+ else:
11105
+ # No renaming needed, proceed directly
11106
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', title = 'Node Communities')
11107
+ self.accept()
11108
+ except:
11109
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', title = 'Node Communities')
11110
+ self.accept()
11111
+ except:
11112
+ import traceback
11113
+ print(traceback.format_exc())
11114
+ pass
11115
+
11116
+ class NeighborhoodRenameDialog(QDialog):
11117
+ def __init__(self, neighborhood_id, averages, node_count, parent=None):
11118
+ super().__init__(parent)
11119
+ self.setWindowTitle(f"Rename Neighborhood {neighborhood_id}")
11120
+ self.setModal(False)
11121
+
11122
+ layout = QVBoxLayout(self)
11123
+
11124
+ # Instructions
11125
+ instructions = QLabel(
11126
+ f"<b>Neighborhood {neighborhood_id}</b><br>"
11127
+ f"Contains {node_count} nodes<br><br>"
11128
+ f"Please review the normalized average marker intensities below and provide a name for this neighborhood:"
11129
+ )
11130
+ instructions.setWordWrap(True)
11131
+ layout.addWidget(instructions)
11132
+
11133
+ # Create scrollable area for averages
11134
+ scroll = QScrollArea()
11135
+ scroll.setWidgetResizable(True)
11136
+ scroll.setMaximumHeight(300)
11137
+
11138
+ averages_widget = QWidget()
11139
+ averages_layout = QVBoxLayout(averages_widget)
11140
+
11141
+ # Display each marker average
11142
+ for marker_name, avg_value in averages.items():
11143
+ label = QLabel(f"{marker_name}: {avg_value:.4f}")
11144
+ averages_layout.addWidget(label)
11145
+
11146
+ scroll.setWidget(averages_widget)
11147
+ layout.addWidget(scroll)
11148
+
11149
+ # Text input for new name
11150
+ layout.addWidget(QLabel("<b>New Neighborhood Name:</b>"))
11151
+ self.name_input = QLineEdit()
11152
+ self.name_input.setPlaceholderText(f"Leave blank to not overwrite node identities for this neighborhood'")
11153
+ layout.addWidget(self.name_input)
11154
+
11155
+ # Buttons
11156
+ from PyQt6.QtWidgets import QDialogButtonBox
11157
+ button_box = QDialogButtonBox(
11158
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
11159
+ )
11160
+ button_box.accepted.connect(self.accept)
11161
+ button_box.rejected.connect(self.reject)
11162
+ layout.addWidget(button_box)
11163
+
11164
+ self.resize(400, 500)
11165
+
11166
+ def get_new_name(self):
11167
+ """Return the new name entered by the user"""
11168
+ return self.name_input.text().strip()
11169
+
10444
11170
 
10445
11171
 
10446
11172
 
@@ -10797,7 +11523,7 @@ class ResizeDialog(QDialog):
10797
11523
 
10798
11524
 
10799
11525
  # cubic checkbox (default False)
10800
- self.cubic = QPushButton("Use Cubic Resize? (Will alter labels and require re-binarization -> labelling, but preserves shape better)")
11526
+ self.cubic = QPushButton("Use Cubic Resize? (For preserving visual characteristics, but not binary shape)")
10801
11527
  self.cubic.setCheckable(True)
10802
11528
  self.cubic.setChecked(False)
10803
11529
  layout.addRow("Use cubic algorithm:", self.cubic)
@@ -10820,7 +11546,7 @@ class ResizeDialog(QDialog):
10820
11546
 
10821
11547
  def run_resize(self, undo = False, upsize = True, special = False):
10822
11548
  try:
10823
- self.parent().resizing = False
11549
+ self.parent().resizing = True
10824
11550
  # Get parameters
10825
11551
  try:
10826
11552
  resize = float(self.resize.text()) if self.resize.text() else None
@@ -10935,6 +11661,7 @@ class ResizeDialog(QDialog):
10935
11661
  if channel is not None:
10936
11662
  self.parent().slice_slider.setMinimum(0)
10937
11663
  self.parent().slice_slider.setMaximum(channel.shape[0] - 1)
11664
+ self.parent().shape = channel.shape
10938
11665
  break
10939
11666
 
10940
11667
  if not special:
@@ -11004,10 +11731,7 @@ class ResizeDialog(QDialog):
11004
11731
  except Exception as e:
11005
11732
  print(f"Error loading edge centroid table: {e}")
11006
11733
 
11007
-
11008
11734
  self.parent().update_display()
11009
- self.reset_fields()
11010
- self.parent().resizing = False
11011
11735
  self.accept()
11012
11736
 
11013
11737
  except Exception as e:
@@ -11016,6 +11740,92 @@ class ResizeDialog(QDialog):
11016
11740
  print(traceback.format_exc())
11017
11741
  QMessageBox.critical(self, "Error", f"Failed to resize: {str(e)}")
11018
11742
 
11743
+ class CleanDialog(QDialog):
11744
+ def __init__(self, parent=None):
11745
+ super().__init__(parent)
11746
+ self.setWindowTitle("Some options for cleaning segmentation")
11747
+ self.setModal(False)
11748
+
11749
+ layout = QFormLayout(self)
11750
+
11751
+ # Add Run button
11752
+ run_button = QPushButton("Close")
11753
+ run_button.clicked.connect(self.close)
11754
+ layout.addRow("Close (Fill Small Gaps - Dilate then Erode by same amount):", run_button)
11755
+
11756
+ # Add Run button
11757
+ run_button = QPushButton("Open")
11758
+ run_button.clicked.connect(self.open)
11759
+ layout.addRow("Open (Eliminate Noise, Jagged Borders, and Small Connections Between Objects - Erode then Dilate by same amount):", run_button)
11760
+
11761
+ # Add Run button
11762
+ run_button = QPushButton("Fill Holes")
11763
+ run_button.clicked.connect(self.holes)
11764
+ layout.addRow("Call the fill holes function:", run_button)
11765
+
11766
+ # Add Run button
11767
+ run_button = QPushButton("Trace Filaments")
11768
+ run_button.clicked.connect(self.fils)
11769
+ layout.addRow("For Segmentations of Blood Vessels/Nerves: ", run_button)
11770
+
11771
+ # Add Run button
11772
+ run_button = QPushButton("Threshold Noise")
11773
+ run_button.clicked.connect(self.thresh)
11774
+ layout.addRow("Threshold Noise By Volume:", run_button)
11775
+
11776
+ def close(self):
11777
+
11778
+ try:
11779
+ self.parent().show_dilate_dialog(args = [1])
11780
+ self.parent().show_erode_dialog(args = [self.parent().last_dil])
11781
+ except:
11782
+ pass
11783
+
11784
+ def open(self):
11785
+
11786
+ try:
11787
+ self.parent().show_erode_dialog(args = [1])
11788
+ self.parent().show_dilate_dialog(args = [self.parent().last_ero])
11789
+ except:
11790
+ pass
11791
+
11792
+ def holes(self):
11793
+
11794
+ try:
11795
+ self.parent().show_hole_dialog()
11796
+ except:
11797
+ pass
11798
+
11799
+ def fils(self):
11800
+
11801
+ try:
11802
+ self.parent().show_filament_dialog()
11803
+ except:
11804
+ self.parent().show_filament_dialog()
11805
+ #pass
11806
+
11807
+ def thresh(self):
11808
+ try:
11809
+ if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
11810
+ self.parent().show_label_dialog()
11811
+
11812
+ if self.parent().volume_dict[self.parent().active_channel] is None:
11813
+ self.parent().volumes()
11814
+
11815
+ thresh_window = ThresholdWindow(self.parent(), 1)
11816
+ thresh_window.show() # Non-modal window
11817
+ self.parent().highlight_overlay = None
11818
+ #self.mini_overlay = False
11819
+ self.parent().mini_overlay_data = None
11820
+ except:
11821
+ import traceback
11822
+ print(traceback.format_exc())
11823
+ pass
11824
+
11825
+
11826
+
11827
+
11828
+
11019
11829
 
11020
11830
  class OverrideDialog(QDialog):
11021
11831
  def __init__(self, parent=None):
@@ -11170,11 +11980,7 @@ class BinarizeDialog(QDialog):
11170
11980
  )
11171
11981
 
11172
11982
  # Update both the display data and the network object
11173
- self.parent().channel_data[self.parent().active_channel] = result
11174
-
11175
-
11176
- # Update the corresponding property in my_network
11177
- setattr(my_network, network_properties[self.parent().active_channel], result)
11983
+ self.parent().load_channel(self.parent().active_channel, result, True)
11178
11984
 
11179
11985
  self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11180
11986
  self.accept()
@@ -11222,11 +12028,7 @@ class LabelDialog(QDialog):
11222
12028
  )
11223
12029
 
11224
12030
  # Update both the display data and the network object
11225
- self.parent().channel_data[self.parent().active_channel] = result
11226
-
11227
-
11228
- # Update the corresponding property in my_network
11229
- setattr(my_network, network_properties[self.parent().active_channel], result)
12031
+ self.parent().load_channel(self.parent().active_channel, result, True)
11230
12032
 
11231
12033
  self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11232
12034
  self.accept()
@@ -11249,7 +12051,7 @@ class LabelDialog(QDialog):
11249
12051
  class SLabelDialog(QDialog):
11250
12052
  def __init__(self, parent=None):
11251
12053
  super().__init__(parent)
11252
- self.setWindowTitle("Smart Label (Use label array to assign label neighborhoods to binary array)?")
12054
+ self.setWindowTitle("Label a binary image based on it's voxels proximity to labeled components of a second image?")
11253
12055
  self.setModal(True)
11254
12056
 
11255
12057
  layout = QFormLayout(self)
@@ -11261,7 +12063,7 @@ class SLabelDialog(QDialog):
11261
12063
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
11262
12064
  layout.addRow("Prelabeled Array:", self.mode_selector)
11263
12065
 
11264
- layout.addRow(QLabel("Will Label Neighborhoods in: "))
12066
+ layout.addRow(QLabel("Will Label Binary Foreground Voxels in: "))
11265
12067
 
11266
12068
  # Add mode selection dropdown
11267
12069
  self.target_selector = QComboBox()
@@ -11273,10 +12075,20 @@ class SLabelDialog(QDialog):
11273
12075
  self.GPU = QPushButton("GPU")
11274
12076
  self.GPU.setCheckable(True)
11275
12077
  self.GPU.setChecked(False)
11276
- layout.addRow("Use GPU:", self.GPU)
12078
+ #layout.addRow("Use GPU:", self.GPU)
11277
12079
 
11278
12080
  self.down_factor = QLineEdit("")
11279
- layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
12081
+ #layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
12082
+
12083
+ self.label_mode = QComboBox()
12084
+ self.label_mode.addItems(["Label Individual Voxels based on Proximity", "Label Continuous Domains that Border Labels"])
12085
+ self.label_mode.setCurrentIndex(0)
12086
+ layout.addRow("Labeling Mode:", self.label_mode)
12087
+
12088
+ self.fix = QPushButton("Correct")
12089
+ self.fix.setCheckable(True)
12090
+ self.fix.setChecked(False)
12091
+ layout.addRow("Correct Nontouching Labels in post (Causes non-contiguous labels to merge with neighbors except the largest instance of that label):", self.fix)
11280
12092
 
11281
12093
  # Add Run button
11282
12094
  run_button = QPushButton("Run Smart Label")
@@ -11289,7 +12101,9 @@ class SLabelDialog(QDialog):
11289
12101
 
11290
12102
  accepted_source = self.mode_selector.currentIndex()
11291
12103
  accepted_target = self.target_selector.currentIndex()
12104
+ label_mode = self.label_mode.currentIndex()
11292
12105
  GPU = self.GPU.isChecked()
12106
+ fix = self.fix.isChecked()
11293
12107
 
11294
12108
 
11295
12109
  if accepted_source == accepted_target:
@@ -11302,27 +12116,51 @@ class SLabelDialog(QDialog):
11302
12116
  down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
11303
12117
 
11304
12118
 
11305
- try:
12119
+ if label_mode == 1:
11306
12120
 
11307
- # Update both the display data and the network object
11308
- binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
12121
+ label_mask = label_array == 0
11309
12122
 
11310
- label_array = sdl.invert_array(label_array)
11311
-
11312
- binary_array = binary_array * label_array
11313
12123
 
12124
+ #if self.parent().shape[0] != 1:
12125
+ # skele = n3d.skeletonize(binary_array)
12126
+ # skele = n3d.fill_holes_3d(skele)
12127
+ skele = n3d.skeletonize(binary_array)
12128
+ skele = label_mask * skele
12129
+ binary_array = label_mask * binary_array
12130
+ del label_mask
12131
+ skele, _ = n3d.label_objects(skele)
12132
+ skele = pxt.label_continuous(skele, label_array)
12133
+ skele = skele + label_array
12134
+ binary_array = sdl.smart_label(binary_array, skele, GPU = False, remove_template = False)
12135
+ binary_array = self.parent().separate_nontouching_objects(binary_array, max_val=np.max(binary_array), branches = True)
12136
+ #binary_array = binary_array + label_array
11314
12137
  self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11315
-
11316
12138
  self.accept()
11317
-
11318
- except Exception as e:
11319
- QMessageBox.critical(
11320
- self,
11321
- "Error",
11322
- f"Error running smart label: {str(e)}"
11323
- )
12139
+
12140
+ else:
12141
+
12142
+ try:
12143
+
12144
+ # Update both the display data and the network object
12145
+ binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
12146
+ if fix:
12147
+ binary_array = self.parent().separate_nontouching_objects(binary_array, max_val=np.max(binary_array), branches = True)
12148
+
12149
+ self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
12150
+
12151
+ self.accept()
12152
+
12153
+ except Exception as e:
12154
+ QMessageBox.critical(
12155
+ self,
12156
+ "Error",
12157
+ f"Error running smart label: {str(e)}"
12158
+ )
11324
12159
 
11325
12160
  except Exception as e:
12161
+ import traceback
12162
+ traceback.print_exc()
12163
+
11326
12164
  QMessageBox.critical(
11327
12165
  self,
11328
12166
  "Error",
@@ -11334,7 +12172,7 @@ class ThresholdDialog(QDialog):
11334
12172
  def __init__(self, parent=None):
11335
12173
  super().__init__(parent)
11336
12174
  self.setWindowTitle("Choose Threshold Mode")
11337
- self.setModal(True)
12175
+ self.setModal(False)
11338
12176
 
11339
12177
  layout = QFormLayout(self)
11340
12178
 
@@ -11523,33 +12361,40 @@ class ExcelotronManager(QObject):
11523
12361
 
11524
12362
  class MachineWindow(QMainWindow):
11525
12363
 
11526
- def __init__(self, parent=None, GPU = False):
12364
+ def __init__(self, parent=None, GPU = False, tutorial_example = False):
11527
12365
  super().__init__(parent)
11528
12366
 
11529
12367
  try:
11530
12368
 
11531
- if self.parent().active_channel == 0:
11532
- if self.parent().channel_data[0] is not None:
11533
- try:
11534
- active_data = self.parent().channel_data[0]
11535
- act_channel = 0
11536
- except:
12369
+ self.tutorial_example = tutorial_example
12370
+
12371
+ if not tutorial_example:
12372
+ if self.parent().active_channel == 0:
12373
+ if self.parent().channel_data[0] is not None:
12374
+ try:
12375
+ active_data = self.parent().channel_data[0]
12376
+ act_channel = 0
12377
+ except:
12378
+ active_data = self.parent().channel_data[1]
12379
+ act_channel = 1
12380
+ else:
11537
12381
  active_data = self.parent().channel_data[1]
11538
12382
  act_channel = 1
11539
- else:
11540
- active_data = self.parent().channel_data[1]
11541
- act_channel = 1
11542
12383
 
11543
- try:
11544
- if len(active_data.shape) == 3:
11545
- array1 = np.zeros_like(active_data).astype(np.uint8)
11546
- elif len(active_data.shape) == 4:
11547
- array1 = np.zeros_like(active_data)[:,:,:,0].astype(np.uint8)
11548
- except:
11549
- print("No data in nodes channel")
11550
- return
12384
+ try:
12385
+ if len(active_data.shape) == 3:
12386
+ array1 = np.zeros_like(active_data).astype(np.uint8)
12387
+ elif len(active_data.shape) == 4:
12388
+ array1 = np.zeros_like(active_data)[:,:,:,0].astype(np.uint8)
12389
+ except:
12390
+ print("No data in nodes channel")
12391
+ return
12392
+
12393
+ if not tutorial_example:
12394
+ self.setWindowTitle("Segmenter")
12395
+ else:
12396
+ self.setWindowTitle("Tutorial Segmenter View (This window will not actually segment)")
11551
12397
 
11552
- self.setWindowTitle("Threshold")
11553
12398
 
11554
12399
  # Create central widget and layout
11555
12400
  central_widget = QWidget()
@@ -11570,22 +12415,23 @@ class MachineWindow(QMainWindow):
11570
12415
 
11571
12416
  self.parent().pen_button.setEnabled(False)
11572
12417
 
11573
- array3 = np.zeros_like(array1).astype(np.uint8)
11574
- self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
12418
+ if not tutorial_example:
12419
+ array3 = np.zeros_like(array1).astype(np.uint8)
12420
+ self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
11575
12421
 
11576
- self.parent().load_channel(2, array1, True)
11577
- # Enable the channel button
11578
- # Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
11579
- if not self.parent().channel_buttons[2].isEnabled():
11580
- self.parent().channel_buttons[2].setEnabled(True)
11581
- self.parent().channel_buttons[2].click()
11582
- self.parent().delete_buttons[2].setEnabled(True)
12422
+ self.parent().load_channel(2, array1, True)
12423
+ # Enable the channel button
12424
+ # Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
12425
+ if not self.parent().channel_buttons[2].isEnabled():
12426
+ self.parent().channel_buttons[2].setEnabled(True)
12427
+ self.parent().channel_buttons[2].click()
12428
+ self.parent().delete_buttons[2].setEnabled(True)
11583
12429
 
11584
- if len(active_data.shape) == 3:
11585
- self.parent().base_colors[act_channel] = self.parent().color_dictionary['WHITE']
11586
- self.parent().base_colors[2] = self.parent().color_dictionary['LIGHT_GREEN']
12430
+ if len(active_data.shape) == 3:
12431
+ self.parent().base_colors[act_channel] = self.parent().color_dictionary['WHITE']
12432
+ self.parent().base_colors[2] = self.parent().color_dictionary['LIGHT_GREEN']
11587
12433
 
11588
- self.parent().update_display()
12434
+ self.parent().update_display()
11589
12435
 
11590
12436
  # Set a reasonable default size for the window
11591
12437
  self.setMinimumWidth(600) # Increased to accommodate grouped buttons
@@ -11791,8 +12637,7 @@ class MachineWindow(QMainWindow):
11791
12637
  self.num_chunks = 0
11792
12638
  self.parent().update_display()
11793
12639
  except:
11794
- import traceback
11795
- traceback.print_exc()
12640
+
11796
12641
  pass
11797
12642
 
11798
12643
  except:
@@ -12191,34 +13036,43 @@ class MachineWindow(QMainWindow):
12191
13036
 
12192
13037
  def closeEvent(self, event):
12193
13038
  try:
12194
- if self.parent() and self.parent().isVisible():
12195
- if self.confirm_close_dialog():
12196
- # Clean up resources before closing
12197
- if self.brush_button.isChecked():
12198
- self.silence_button()
12199
- self.toggle_brush_mode()
12200
-
12201
- self.parent().pen_button.setEnabled(True)
12202
- self.parent().brush_mode = False
12203
-
12204
- # Kill the segmentation thread and wait for it to finish
12205
- self.kill_segmentation()
12206
- time.sleep(0.2) # Give additional time for cleanup
12207
- try:
12208
- self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
12209
- self.update_display()
12210
- except:
12211
- pass
12212
-
12213
- self.parent().machine_window = None
12214
- event.accept() # IMPORTANT: Accept the close event
13039
+ if not self.tutorial_example:
13040
+ if self.parent() and self.parent().isVisible():
13041
+ if self.confirm_close_dialog():
13042
+ # Clean up resources before closing
13043
+ if self.brush_button.isChecked():
13044
+ self.silence_button()
13045
+ self.toggle_brush_mode()
13046
+
13047
+ self.parent().pen_button.setEnabled(True)
13048
+ self.parent().brush_mode = False
13049
+
13050
+ # Kill the segmentation thread and wait for it to finish
13051
+ self.kill_segmentation()
13052
+ time.sleep(0.2) # Give additional time for cleanup
13053
+ try:
13054
+ self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
13055
+ self.update_display()
13056
+ except:
13057
+ pass
13058
+
13059
+ self.parent().machine_window = None
13060
+ event.accept() # IMPORTANT: Accept the close event
13061
+ else:
13062
+ event.ignore() # User cancelled, ignore the close
12215
13063
  else:
12216
- event.ignore() # User cancelled, ignore the close
13064
+ # Parent doesn't exist or isn't visible, just close
13065
+ if hasattr(self, 'parent') and self.parent():
13066
+ self.parent().machine_window = None
13067
+ event.accept()
12217
13068
  else:
12218
- # Parent doesn't exist or isn't visible, just close
12219
- if hasattr(self, 'parent') and self.parent():
12220
- self.parent().machine_window = None
12221
- event.accept()
13069
+ self.parent().machine_window = None
13070
+ if self.brush_button.isChecked():
13071
+ self.silence_button()
13072
+ self.toggle_brush_mode()
13073
+ self.parent().pen_button.setEnabled(True)
13074
+ self.parent().brush_mode = False
13075
+
12222
13076
  except Exception as e:
12223
13077
  print(f"Error in closeEvent: {e}")
12224
13078
  # Even if there's an error, allow the window to close
@@ -12326,6 +13180,7 @@ class ThresholdWindow(QMainWindow):
12326
13180
 
12327
13181
  def __init__(self, parent=None, accepted_mode=0):
12328
13182
  super().__init__(parent)
13183
+ self.parent().thresh_window_ref = self
12329
13184
  self.setWindowTitle("Threshold")
12330
13185
 
12331
13186
  self.accepted_mode = accepted_mode
@@ -12370,19 +13225,28 @@ class ThresholdWindow(QMainWindow):
12370
13225
  data = self.parent().channel_data[self.parent().active_channel]
12371
13226
  nonzero_data = data[data != 0]
12372
13227
 
12373
- if nonzero_data.size > 578009537:
12374
- # For large arrays, use numpy histogram directly
12375
- counts, bin_edges = np.histogram(nonzero_data, bins= min(int(np.sqrt(nonzero_data.size)), 500), density=False)
12376
- # Store min/max separately if needed elsewhere
12377
- self.data_min = np.min(nonzero_data)
12378
- self.data_max = np.max(nonzero_data)
12379
- self.histo_list = [self.data_min, self.data_max]
13228
+ MAX_SAMPLES_FOR_HISTOGRAM = 10_000_000 # Downsample data above this size
13229
+ MAX_HISTOGRAM_BINS = 512 # Maximum bins for smooth matplotlib interaction
13230
+ MIN_HISTOGRAM_BINS = 128 # Minimum bins for decent resolution
13231
+
13232
+ # Always compute min/max first (before any downsampling)
13233
+ self.data_min = np.min(nonzero_data)
13234
+ self.data_max = np.max(nonzero_data)
13235
+ self.histo_list = [self.data_min, self.data_max]
13236
+
13237
+ # Downsample data if too large
13238
+ if nonzero_data.size > MAX_SAMPLES_FOR_HISTOGRAM:
13239
+ downsample_factor = int(np.ceil(nonzero_data.size / MAX_SAMPLES_FOR_HISTOGRAM))
13240
+ nonzero_data_sampled = n3d.downsample(nonzero_data, downsample_factor)
12380
13241
  else:
12381
- # For smaller arrays, can still use histogram method for consistency
12382
- counts, bin_edges = np.histogram(nonzero_data, bins='auto', density=False)
12383
- self.data_min = np.min(nonzero_data)
12384
- self.data_max = np.max(nonzero_data)
12385
- self.histo_list = [self.data_min, self.data_max]
13242
+ nonzero_data_sampled = nonzero_data
13243
+
13244
+ # Calculate optimal bin count (capped for matplotlib performance)
13245
+ # Using Sturges' rule but capped to reasonable limits
13246
+ n_bins = int(np.ceil(np.log2(nonzero_data_sampled.size)) + 1)
13247
+ n_bins = np.clip(n_bins, MIN_HISTOGRAM_BINS, MAX_HISTOGRAM_BINS)
13248
+
13249
+ counts, bin_edges = np.histogram(nonzero_data_sampled, bins=n_bins, density=False)
12386
13250
 
12387
13251
  self.bounds = True
12388
13252
  self.parent().bounds = True
@@ -12479,10 +13343,8 @@ class ThresholdWindow(QMainWindow):
12479
13343
  self.processing_cancelled.emit()
12480
13344
  self.close()
12481
13345
 
12482
- def closeEvent(self, event):
12483
- self.parent().preview = False
12484
- self.parent().targs = None
12485
- self.parent().bounds = False
13346
+ def make_full_highlight(self):
13347
+
12486
13348
  try: # could probably be refactored but this just handles keeping the highlight elements if the user presses X
12487
13349
  if self.chan == 0:
12488
13350
  if not self.bounds:
@@ -12516,6 +13378,14 @@ class ThresholdWindow(QMainWindow):
12516
13378
  pass
12517
13379
 
12518
13380
 
13381
+ def closeEvent(self, event):
13382
+ self.parent().preview = False
13383
+ self.parent().targs = None
13384
+ self.parent().bounds = False
13385
+ self.parent().thresh_window_ref = None
13386
+ self.make_full_highlight()
13387
+
13388
+
12519
13389
  def get_values_in_range_all_vols(self, chan, min_val, max_val):
12520
13390
  output = []
12521
13391
  if self.accepted_mode == 1:
@@ -12782,36 +13652,33 @@ class SmartDilateDialog(QDialog):
12782
13652
 
12783
13653
 
12784
13654
  class DilateDialog(QDialog):
12785
- def __init__(self, parent=None):
13655
+ def __init__(self, parent=None, args = None):
12786
13656
  super().__init__(parent)
12787
13657
  self.setWindowTitle("Dilate Parameters")
12788
- self.setModal(True)
13658
+ self.setModal(False)
12789
13659
 
12790
13660
  layout = QFormLayout(self)
12791
13661
 
12792
- self.amount = QLineEdit("1")
12793
- layout.addRow("Dilation Radius:", self.amount)
12794
-
12795
- if my_network.xy_scale is not None:
12796
- xy_scale = f"{my_network.xy_scale}"
13662
+ if args:
13663
+ self.parent().last_dil = args[0]
13664
+ self.index = 1
12797
13665
  else:
12798
- xy_scale = "1"
13666
+ self.parent().last_dil = 1
13667
+ self.index = 0
12799
13668
 
12800
- self.xy_scale = QLineEdit(xy_scale)
12801
- layout.addRow("xy_scale:", self.xy_scale)
13669
+ self.amount = QLineEdit(f"{self.parent().last_dil}")
13670
+ layout.addRow("Dilation Radius:", self.amount)
12802
13671
 
12803
- if my_network.z_scale is not None:
12804
- z_scale = f"{my_network.z_scale}"
12805
- else:
12806
- z_scale = "1"
13672
+ self.xy_scale = QLineEdit("1")
13673
+ layout.addRow("xy_scale:", self.xy_scale)
12807
13674
 
12808
- self.z_scale = QLineEdit(z_scale)
13675
+ self.z_scale = QLineEdit("1")
12809
13676
  layout.addRow("z_scale:", self.z_scale)
12810
13677
 
12811
13678
  # Add mode selection dropdown
12812
13679
  self.mode_selector = QComboBox()
12813
- self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small dilations)", "Preserve Labels (slower)", "Distance Transform-Based (Slower but more accurate at larger dilations)"])
12814
- self.mode_selector.setCurrentIndex(0) # Default to Mode 1
13680
+ self.mode_selector.addItems(["Distance Transform-Based (Slower but more accurate at larger dilations)", "Preserve Labels (slower)", "Pseudo3D Binary Kernels (For Fast, small dilations)"])
13681
+ self.mode_selector.setCurrentIndex(self.index) # Default to Mode 1
12815
13682
  layout.addRow("Execution Mode:", self.mode_selector)
12816
13683
 
12817
13684
  # Add Run button
@@ -12853,13 +13720,15 @@ class DilateDialog(QDialog):
12853
13720
  if active_data is None:
12854
13721
  raise ValueError("No active image selected")
12855
13722
 
13723
+ self.parent().last_dil = amount
13724
+
12856
13725
  if accepted_mode == 1:
12857
13726
  dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
12858
13727
  dialog.exec()
12859
13728
  self.accept()
12860
13729
  return
12861
13730
 
12862
- if accepted_mode == 2:
13731
+ if accepted_mode == 0:
12863
13732
  result = n3d.dilate_3D_dt(active_data, amount, xy_scaling = xy_scale, z_scaling = z_scale)
12864
13733
  else:
12865
13734
 
@@ -12889,36 +13758,33 @@ class DilateDialog(QDialog):
12889
13758
  )
12890
13759
 
12891
13760
  class ErodeDialog(QDialog):
12892
- def __init__(self, parent=None):
13761
+ def __init__(self, parent=None, args = None):
12893
13762
  super().__init__(parent)
12894
13763
  self.setWindowTitle("Erosion Parameters")
12895
13764
  self.setModal(True)
12896
13765
 
12897
13766
  layout = QFormLayout(self)
12898
13767
 
12899
- self.amount = QLineEdit("1")
12900
- layout.addRow("Erosion Radius:", self.amount)
12901
-
12902
- if my_network.xy_scale is not None:
12903
- xy_scale = f"{my_network.xy_scale}"
13768
+ if args:
13769
+ self.parent().last_ero = args[0]
13770
+ self.index = 1
12904
13771
  else:
12905
- xy_scale = "1"
13772
+ self.parent().last_ero = 1
13773
+ self.index = 0
12906
13774
 
12907
- self.xy_scale = QLineEdit(xy_scale)
12908
- layout.addRow("xy_scale:", self.xy_scale)
13775
+ self.amount = QLineEdit(f"{self.parent().last_ero}")
13776
+ layout.addRow("Erosion Radius:", self.amount)
12909
13777
 
12910
- if my_network.z_scale is not None:
12911
- z_scale = f"{my_network.z_scale}"
12912
- else:
12913
- z_scale = "1"
13778
+ self.xy_scale = QLineEdit("1")
13779
+ layout.addRow("xy_scale:", self.xy_scale)
12914
13780
 
12915
- self.z_scale = QLineEdit(z_scale)
13781
+ self.z_scale = QLineEdit("1")
12916
13782
  layout.addRow("z_scale:", self.z_scale)
12917
13783
 
12918
13784
  # Add mode selection dropdown
12919
13785
  self.mode_selector = QComboBox()
12920
- self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small erosions)", "Distance Transform-Based (Slower but more accurate at larger dilations)", "Preserve Labels (Slower)"])
12921
- self.mode_selector.setCurrentIndex(0) # Default to Mode 1
13786
+ self.mode_selector.addItems(["Distance Transform-Based (Slower but more accurate at larger erosions)", "Preserve Labels (Slower)", "Pseudo3D Binary Kernels (For Fast, small erosions)"])
13787
+ self.mode_selector.setCurrentIndex(self.index) # Default to Mode 1
12922
13788
  layout.addRow("Execution Mode:", self.mode_selector)
12923
13789
 
12924
13790
  # Add Run button
@@ -12954,8 +13820,7 @@ class ErodeDialog(QDialog):
12954
13820
 
12955
13821
  mode = self.mode_selector.currentIndex()
12956
13822
 
12957
- if mode == 2:
12958
- mode = 1
13823
+ if mode == 1:
12959
13824
  preserve_labels = True
12960
13825
  else:
12961
13826
  preserve_labels = False
@@ -12977,7 +13842,7 @@ class ErodeDialog(QDialog):
12977
13842
 
12978
13843
 
12979
13844
  self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
12980
-
13845
+ self.parent().last_ero = amount
12981
13846
  self.accept()
12982
13847
 
12983
13848
  except Exception as e:
@@ -13007,6 +13872,11 @@ class HoleDialog(QDialog):
13007
13872
  self.borders.setChecked(False)
13008
13873
  layout.addRow("Fill Small Holes Along Borders:", self.borders)
13009
13874
 
13875
+ self.preserve_labels = QPushButton("Preserve Labels")
13876
+ self.preserve_labels.setCheckable(True)
13877
+ self.preserve_labels.setChecked(False)
13878
+ layout.addRow("Preserve Labels (Slower):", self.preserve_labels)
13879
+
13010
13880
  self.sep_holes = QPushButton("Seperate Hole Mask")
13011
13881
  self.sep_holes.setCheckable(True)
13012
13882
  self.sep_holes.setChecked(False)
@@ -13029,6 +13899,9 @@ class HoleDialog(QDialog):
13029
13899
  borders = self.borders.isChecked()
13030
13900
  headon = self.headon.isChecked()
13031
13901
  sep_holes = self.sep_holes.isChecked()
13902
+ preserve_labels = self.preserve_labels.isChecked()
13903
+ if preserve_labels:
13904
+ label_copy = np.copy(active_data)
13032
13905
 
13033
13906
  if borders:
13034
13907
 
@@ -13047,7 +13920,11 @@ class HoleDialog(QDialog):
13047
13920
  fill_borders = borders
13048
13921
  )
13049
13922
 
13923
+
13050
13924
  if not sep_holes:
13925
+ if preserve_labels:
13926
+ result = sdl.smart_label(result, label_copy, directory = None, GPU = False, remove_template = True)
13927
+
13051
13928
  self.parent().load_channel(self.parent().active_channel, result, True)
13052
13929
  else:
13053
13930
  self.parent().load_channel(3, active_data - result, True)
@@ -13063,6 +13940,135 @@ class HoleDialog(QDialog):
13063
13940
  f"Error running fill holes: {str(e)}"
13064
13941
  )
13065
13942
 
13943
+ class FilamentDialog(QDialog):
13944
+ def __init__(self, parent=None):
13945
+ super().__init__(parent)
13946
+ self.setWindowTitle("Parameters for Vessel Tracer (Note none of these are scaled with xy or z scale properties)")
13947
+ self.setModal(False)
13948
+
13949
+ main_layout = QVBoxLayout(self)
13950
+
13951
+ # Speedup Group
13952
+ speedup_group = QGroupBox("Speedup")
13953
+ speedup_layout = QFormLayout()
13954
+ self.kernel_spacing = QLineEdit("3")
13955
+ speedup_layout.addRow("Kernel Spacing (1 is most accurate, can increase to speed up):", self.kernel_spacing)
13956
+ self.downsample_factor = QLineEdit("1")
13957
+ speedup_layout.addRow("Temporary Downsample Factor (Note that the below distances are not adjusted for this):", self.downsample_factor)
13958
+ speedup_group.setLayout(speedup_layout)
13959
+ main_layout.addWidget(speedup_group)
13960
+
13961
+ # Reconnection Behavior Group
13962
+ reconnection_group = QGroupBox("Reconnection Behavior")
13963
+ reconnection_layout = QFormLayout()
13964
+ self.max_distance = QLineEdit("20")
13965
+ reconnection_layout.addRow("Max Distance to Consider Connecting Filaments (Will Slow Down a lot if Large):", self.max_distance)
13966
+ self.gap_tolerance = QLineEdit("5")
13967
+ reconnection_layout.addRow("Gap Tolerance. Higher Values Increase Likelihood of Connecting over Larger Gaps:", self.gap_tolerance)
13968
+ self.score_threshold = QLineEdit("2")
13969
+ reconnection_layout.addRow("Connection Quality Threshold. Lower Values Increase Likelihood of Connecting In General, can be Negative:", self.score_threshold)
13970
+ reconnection_group.setLayout(reconnection_layout)
13971
+ main_layout.addWidget(reconnection_group)
13972
+
13973
+ # Artifact Removal Group
13974
+ artifact_group = QGroupBox("Artifact Removal")
13975
+ artifact_layout = QFormLayout()
13976
+ self.min_component = QLineEdit("20")
13977
+ artifact_layout.addRow("Minimum Component Size to Include:", self.min_component)
13978
+ self.blob_sphericity = QLineEdit("1.0")
13979
+ artifact_layout.addRow("Spherical Objects in the Output can Represent Noise. Enter a val 0 < x < 1 to consider removing spheroids. Larger vals are more spherical. 1.0 = a perfect sphere. 0.3 is usually the lower bound of a spheroid:", self.blob_sphericity)
13980
+ self.blob_volume = QLineEdit("200")
13981
+ artifact_layout.addRow("If filtering spheroids: Minimum Volume of Spheroid to Remove (Smaller spheroids may be real):", self.blob_volume)
13982
+ self.spine_removal = QLineEdit("0")
13983
+ artifact_layout.addRow("Remove Branch Spines Below this Length?", self.spine_removal)
13984
+ artifact_group.setLayout(artifact_layout)
13985
+ main_layout.addWidget(artifact_group)
13986
+
13987
+
13988
+ # Run Button
13989
+ run_button = QPushButton("Run Filament Tracer (Output Goes in Overlay 2)")
13990
+ run_button.clicked.connect(self.run)
13991
+ main_layout.addWidget(run_button)
13992
+
13993
+
13994
+ def run(self):
13995
+
13996
+ try:
13997
+
13998
+ from . import filaments
13999
+
14000
+
14001
+ kernel_spacing = int(self.kernel_spacing.text()) if self.kernel_spacing.text().strip() else 1
14002
+ max_distance = float(self.max_distance.text()) if self.max_distance.text().strip() else 20
14003
+ min_component = int(self.min_component.text()) if self.min_component.text().strip() else 20
14004
+ gap_tolerance = float(self.gap_tolerance.text()) if self.gap_tolerance.text().strip() else 5
14005
+ blob_sphericity = float(self.blob_sphericity.text()) if self.blob_sphericity.text().strip() else 1
14006
+ blob_volume = float(self.blob_volume.text()) if self.blob_volume.text().strip() else 200
14007
+ spine_removal = int(self.spine_removal.text()) if self.spine_removal.text().strip() else 0
14008
+ score_threshold = int(self.score_threshold.text()) if self.score_threshold.text().strip() else 0
14009
+ downsample_factor = int(self.downsample_factor.text()) if self.downsample_factor.text().strip() else None
14010
+ data = self.parent().channel_data[self.parent().active_channel]
14011
+
14012
+ if downsample_factor and downsample_factor > 1:
14013
+ data = n3d.downsample(data, downsample_factor)
14014
+
14015
+ result = filaments.trace(data, kernel_spacing, max_distance, min_component, gap_tolerance, blob_sphericity, blob_volume, spine_removal, score_threshold)
14016
+
14017
+ if downsample_factor and downsample_factor > 1:
14018
+
14019
+ result = n3d.upsample_with_padding(result, original_shape = self.parent().shape)
14020
+
14021
+
14022
+ self.parent().load_channel(3, result, True)
14023
+
14024
+ self.accept()
14025
+
14026
+ except Exception as e:
14027
+ import traceback
14028
+ print(traceback.format_exc())
14029
+ print(f"Error: {e}")
14030
+
14031
+ def wait_for_threshold_processing(self):
14032
+ """
14033
+ Opens ThresholdWindow and waits for user to process the image.
14034
+ Returns True if completed, False if cancelled.
14035
+ The thresholded image will be available in the main window after completion.
14036
+ """
14037
+ # Create event loop to wait for user
14038
+ loop = QEventLoop()
14039
+ result = {'completed': False}
14040
+
14041
+ # Create the threshold window
14042
+ thresh_window = ThresholdWindow(self.parent(), 0)
14043
+
14044
+
14045
+ # Connect signals
14046
+ def on_processing_complete():
14047
+ result['completed'] = True
14048
+ loop.quit()
14049
+
14050
+ def on_processing_cancelled():
14051
+ result['completed'] = False
14052
+ loop.quit()
14053
+
14054
+ thresh_window.processing_complete.connect(on_processing_complete)
14055
+ thresh_window.processing_cancelled.connect(on_processing_cancelled)
14056
+
14057
+ # Show window and wait
14058
+ thresh_window.show()
14059
+ thresh_window.raise_()
14060
+ thresh_window.activateWindow()
14061
+
14062
+ # Block until user clicks "Apply Threshold & Continue" or "Cancel"
14063
+ loop.exec()
14064
+
14065
+ # Clean up
14066
+ thresh_window.deleteLater()
14067
+
14068
+ return result['completed']
14069
+
14070
+
14071
+
13066
14072
  class MaskDialog(QDialog):
13067
14073
 
13068
14074
  def __init__(self, parent=None):
@@ -13392,7 +14398,13 @@ class SkeletonizeDialog(QDialog):
13392
14398
  # auto checkbox (default True)
13393
14399
  self.auto = QPushButton("Auto")
13394
14400
  self.auto.setCheckable(True)
13395
- self.auto.setChecked(False)
14401
+ try:
14402
+ if self.shape[0] == 1:
14403
+ self.auto.setChecked(False)
14404
+ else:
14405
+ self.auto.setChecked(True)
14406
+ except:
14407
+ self.auto.setChecked(True)
13396
14408
  layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
13397
14409
 
13398
14410
  # Add Run button
@@ -13448,6 +14460,86 @@ class SkeletonizeDialog(QDialog):
13448
14460
  f"Error running skeletonize: {str(e)}"
13449
14461
  )
13450
14462
 
14463
+
14464
+ class BranchStatDialog(QDialog):
14465
+
14466
+ def __init__(self, parent=None):
14467
+ super().__init__(parent)
14468
+ self.setWindowTitle("Make sure branches are labeled first (Image -> Generate -> Label Branches)")
14469
+ self.setModal(True)
14470
+
14471
+ layout = QFormLayout(self)
14472
+
14473
+ info_label = QLabel("Skeletonization Params for Getting Branch Stats, Make sure xy and z scale are set correctly in properties")
14474
+ layout.addRow(info_label)
14475
+
14476
+ self.remove = QLineEdit("0")
14477
+ layout.addRow("Remove Branches Pixel Length (int):", self.remove)
14478
+
14479
+ # auto checkbox (default True)
14480
+ self.auto = QPushButton("Auto")
14481
+ self.auto.setCheckable(True)
14482
+ try:
14483
+ if self.shape[0] == 1:
14484
+ self.auto.setChecked(False)
14485
+ else:
14486
+ self.auto.setChecked(True)
14487
+ except:
14488
+ self.auto.setChecked(True)
14489
+ layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
14490
+
14491
+ # Add Run button
14492
+ run_button = QPushButton("Get Branchstats (For Active Image)")
14493
+ run_button.clicked.connect(self.run)
14494
+ layout.addRow(run_button)
14495
+
14496
+ def run(self):
14497
+
14498
+ try:
14499
+
14500
+ # Get branch removal
14501
+ try:
14502
+ remove = int(self.remove.text()) if self.remove.text() else 0
14503
+ except ValueError:
14504
+ remove = 0
14505
+
14506
+ auto = self.auto.isChecked()
14507
+
14508
+ # Get the active channel data from parent
14509
+ active_data = np.copy(self.parent().channel_data[self.parent().active_channel])
14510
+ if active_data is None:
14511
+ raise ValueError("No active image selected")
14512
+
14513
+ if auto:
14514
+ active_data = n3d.skeletonize(active_data)
14515
+ active_data = n3d.fill_holes_3d(active_data)
14516
+
14517
+ active_data = n3d.skeletonize(
14518
+ active_data
14519
+ )
14520
+
14521
+ if remove > 0:
14522
+ active_data = n3d.remove_branches_new(active_data, remove)
14523
+ active_data = n3d.dilate_3D(active_data, 3, 3, 3)
14524
+ active_data = n3d.skeletonize(active_data)
14525
+
14526
+ active_data = active_data * self.parent().channel_data[self.parent().active_channel]
14527
+ len_dict, tortuosity_dict, angle_dict = n3d.compute_optional_branchstats(None, active_data, None, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
14528
+
14529
+ if self.parent().active_channel == 0:
14530
+ self.parent().branch_dict[0] = [len_dict, tortuosity_dict]
14531
+ elif self.parent().active_channel == 1:
14532
+ self.parent().branch_dict[1] = [len_dict, tortuosity_dict]
14533
+
14534
+ self.parent().format_for_upperright_table(len_dict, 'BranchID', 'Length (Scaled)', 'Branch Lengths')
14535
+ self.parent().format_for_upperright_table(tortuosity_dict, 'BranchID', 'Tortuosity', 'Branch Tortuosities')
14536
+
14537
+
14538
+ self.accept()
14539
+
14540
+ except Exception as e:
14541
+ print(f"Error: {e}")
14542
+
13451
14543
  class DistanceDialog(QDialog):
13452
14544
  def __init__(self, parent=None):
13453
14545
  super().__init__(parent)
@@ -13566,11 +14658,6 @@ class WatershedDialog(QDialog):
13566
14658
  self.setModal(True)
13567
14659
 
13568
14660
  layout = QFormLayout(self)
13569
-
13570
- # Directory (empty by default)
13571
- self.directory = QLineEdit()
13572
- self.directory.setPlaceholderText("Leave empty for None")
13573
- layout.addRow("Output Directory:", self.directory)
13574
14661
 
13575
14662
  try:
13576
14663
 
@@ -13621,7 +14708,7 @@ class WatershedDialog(QDialog):
13621
14708
  def run_watershed(self):
13622
14709
  try:
13623
14710
  # Get directory (None if empty)
13624
- directory = self.directory.text() if self.directory.text() else None
14711
+ directory = None
13625
14712
 
13626
14713
  # Get proportion (0.1 if empty or invalid)
13627
14714
  try:
@@ -13865,7 +14952,7 @@ class GenNodesDialog(QDialog):
13865
14952
  def __init__(self, parent=None, down_factor=None, called=False):
13866
14953
  super().__init__(parent)
13867
14954
  self.setWindowTitle("Create Nodes from Edge Vertices")
13868
- self.setModal(True)
14955
+ self.setModal(False)
13869
14956
 
13870
14957
  # Main layout
13871
14958
  main_layout = QVBoxLayout(self)
@@ -13889,15 +14976,15 @@ class GenNodesDialog(QDialog):
13889
14976
  self.cubic = QPushButton("Cubic Downsample")
13890
14977
  self.cubic.setCheckable(True)
13891
14978
  self.cubic.setChecked(False)
13892
- process_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
13893
- process_layout.addWidget(self.cubic, 1, 1)
14979
+ #process_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
14980
+ #process_layout.addWidget(self.cubic, 1, 1)
13894
14981
 
13895
14982
  # Fast dilation checkbox
13896
14983
  self.fast_dil = QPushButton("Fast-Dil")
13897
14984
  self.fast_dil.setCheckable(True)
13898
14985
  self.fast_dil.setChecked(True)
13899
- process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 2, 0)
13900
- process_layout.addWidget(self.fast_dil, 2, 1)
14986
+ #process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 2, 0)
14987
+ #process_layout.addWidget(self.fast_dil, 2, 1)
13901
14988
 
13902
14989
  process_group.setLayout(process_layout)
13903
14990
  main_layout.addWidget(process_group)
@@ -13906,17 +14993,17 @@ class GenNodesDialog(QDialog):
13906
14993
  self.cubic = down_factor[1]
13907
14994
 
13908
14995
  # Fast dilation checkbox (still needed even if down_factor is provided)
13909
- process_group = QGroupBox("Processing Options")
13910
- process_layout = QGridLayout()
14996
+ #process_group = QGroupBox("Processing Options")
14997
+ #process_layout = QGridLayout()
13911
14998
 
13912
14999
  self.fast_dil = QPushButton("Fast-Dil")
13913
15000
  self.fast_dil.setCheckable(True)
13914
15001
  self.fast_dil.setChecked(True)
13915
- process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 0, 0)
13916
- process_layout.addWidget(self.fast_dil, 0, 1)
15002
+ #process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 0, 0)
15003
+ #process_layout.addWidget(self.fast_dil, 0, 1)
13917
15004
 
13918
- process_group.setLayout(process_layout)
13919
- main_layout.addWidget(process_group)
15005
+ #process_group.setLayout(process_layout)
15006
+ #main_layout.addWidget(process_group)
13920
15007
 
13921
15008
  # --- Recommended Corrections Group ---
13922
15009
  rec_group = QGroupBox("Recommended Corrections")
@@ -13949,8 +15036,8 @@ class GenNodesDialog(QDialog):
13949
15036
 
13950
15037
  # Max volume
13951
15038
  self.max_vol = QLineEdit("0")
13952
- opt_layout.addWidget(QLabel("Maximum Voxel Volume to Retain (Compensates for skeleton looping):"), 0, 0)
13953
- opt_layout.addWidget(self.max_vol, 0, 1)
15039
+ #opt_layout.addWidget(QLabel("Maximum Voxel Volume to Retain (Compensates for skeleton looping):"), 0, 0)
15040
+ #opt_layout.addWidget(self.max_vol, 0, 1)
13954
15041
 
13955
15042
  # Component dilation
13956
15043
  self.comp_dil = QLineEdit("0")
@@ -14026,6 +15113,8 @@ class GenNodesDialog(QDialog):
14026
15113
 
14027
15114
  fastdil = self.fast_dil.isChecked()
14028
15115
 
15116
+ if down_factor > 1:
15117
+ my_network.edges = n3d.downsample(my_network.edges, down_factor)
14029
15118
 
14030
15119
  if auto:
14031
15120
  my_network.edges = n3d.skeletonize(my_network.edges)
@@ -14037,11 +15126,9 @@ class GenNodesDialog(QDialog):
14037
15126
  max_vol=max_vol,
14038
15127
  branch_removal=branch_removal,
14039
15128
  comp_dil=comp_dil,
14040
- down_factor=down_factor,
14041
15129
  order = order,
14042
15130
  return_skele = True,
14043
15131
  fastdil = fastdil
14044
-
14045
15132
  )
14046
15133
 
14047
15134
  if down_factor > 0 and not self.called:
@@ -14092,10 +15179,10 @@ class GenNodesDialog(QDialog):
14092
15179
 
14093
15180
  class BranchDialog(QDialog):
14094
15181
 
14095
- def __init__(self, parent=None, called = False):
15182
+ def __init__(self, parent=None, called = False, tutorial_example = False):
14096
15183
  super().__init__(parent)
14097
15184
  self.setWindowTitle("Label Branches (of edges)")
14098
- self.setModal(True)
15185
+ self.setModal(False)
14099
15186
 
14100
15187
  # Main layout
14101
15188
  main_layout = QVBoxLayout(self)
@@ -14108,33 +15195,40 @@ class BranchDialog(QDialog):
14108
15195
  self.fix = QPushButton("Auto-Correct 1")
14109
15196
  self.fix.setCheckable(True)
14110
15197
  self.fix.setChecked(False)
14111
- correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
14112
- correction_layout.addWidget(self.fix, 0, 1)
15198
+ #correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
15199
+ #correction_layout.addWidget(self.fix, 0, 1)
14113
15200
 
14114
15201
  # Fix value
14115
15202
  self.fix_val = QLineEdit('4')
14116
- correction_layout.addWidget(QLabel("Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
14117
- correction_layout.addWidget(self.fix_val, 1, 1)
15203
+ #correction_layout.addWidget(QLabel("(For Auto-Correct 1) Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
15204
+ #correction_layout.addWidget(self.fix_val, 1, 1)
14118
15205
 
14119
15206
  # Seed
14120
15207
  self.seed = QLineEdit('')
14121
- correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
14122
- correction_layout.addWidget(self.seed, 2, 1)
15208
+ #correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
15209
+ #correction_layout.addWidget(self.seed, 2, 1)
14123
15210
 
14124
- self.fix2 = QPushButton("Auto-Correct 2")
15211
+ self.fix2 = QPushButton("Auto-Correct Internal Branches")
14125
15212
  self.fix2.setCheckable(True)
14126
15213
  self.fix2.setChecked(True)
14127
15214
  correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Internal Labels: "), 3, 0)
14128
15215
  correction_layout.addWidget(self.fix2, 3, 1)
14129
15216
 
14130
- self.fix3 = QPushButton("Split Nontouching Branches?")
15217
+ self.fix3 = QPushButton("Auto-Correct Nontouching Branches")
14131
15218
  self.fix3.setCheckable(True)
14132
- if called:
14133
- self.fix3.setChecked(True)
14134
- else:
14135
- self.fix3.setChecked(False)
14136
- correction_layout.addWidget(QLabel("Split Nontouching Branches? (Useful if branch pruning - may want to threshold out small, split branches after): "), 4, 0)
15219
+ self.fix3.setChecked(True)
15220
+ correction_layout.addWidget(QLabel("Auto-Correct Nontouching Branches?: "), 4, 0)
14137
15221
  correction_layout.addWidget(self.fix3, 4, 1)
15222
+
15223
+ self.fix4 = QPushButton("Auto-Attempt to Reunify Main Branches?")
15224
+ self.fix4.setCheckable(True)
15225
+ self.fix4.setChecked(False)
15226
+ correction_layout.addWidget(QLabel("Reunify Main Branches: "), 5, 0)
15227
+ correction_layout.addWidget(self.fix4, 5, 1)
15228
+
15229
+ self.fix4_val = QLineEdit('10')
15230
+ correction_layout.addWidget(QLabel("(For Reunify) Minimum Score to Merge? (Lower vals = More mergers, can be negative):"), 6, 0)
15231
+ correction_layout.addWidget(self.fix4_val, 6, 1)
14138
15232
 
14139
15233
  correction_group.setLayout(correction_layout)
14140
15234
  main_layout.addWidget(correction_group)
@@ -14152,8 +15246,8 @@ class BranchDialog(QDialog):
14152
15246
  self.cubic = QPushButton("Cubic Downsample")
14153
15247
  self.cubic.setCheckable(True)
14154
15248
  self.cubic.setChecked(False)
14155
- processing_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
14156
- processing_layout.addWidget(self.cubic, 1, 1)
15249
+ #processing_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
15250
+ #processing_layout.addWidget(self.cubic, 1, 1)
14157
15251
 
14158
15252
  processing_group.setLayout(processing_layout)
14159
15253
  main_layout.addWidget(processing_group)
@@ -14161,20 +15255,27 @@ class BranchDialog(QDialog):
14161
15255
  # --- Misc Options Group ---
14162
15256
  misc_group = QGroupBox("Misc Options")
14163
15257
  misc_layout = QGridLayout()
15258
+
15259
+ # optional computation checkbox
15260
+ self.compute = QPushButton("Branch Stats")
15261
+ self.compute.setCheckable(True)
15262
+ self.compute.setChecked(True)
15263
+ misc_layout.addWidget(QLabel("Compute Branch Stats (Branch Lengths, Tortuosity. Set xy_scale and z_scale in properties first if real distances are desired.):"), 0, 0)
15264
+ misc_layout.addWidget(self.compute, 0, 1)
14164
15265
 
14165
15266
  # Nodes checkbox
14166
15267
  self.nodes = QPushButton("Generate Nodes")
14167
15268
  self.nodes.setCheckable(True)
14168
15269
  self.nodes.setChecked(True)
14169
- misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"), 0, 0)
14170
- misc_layout.addWidget(self.nodes, 0, 1)
15270
+ misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"), 1, 0)
15271
+ misc_layout.addWidget(self.nodes, 1, 1)
14171
15272
 
14172
15273
  # GPU checkbox
14173
15274
  self.GPU = QPushButton("GPU")
14174
15275
  self.GPU.setCheckable(True)
14175
15276
  self.GPU.setChecked(False)
14176
- misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"), 1, 0)
14177
- misc_layout.addWidget(self.GPU, 1, 1)
15277
+ #misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"), 2, 0)
15278
+ #misc_layout.addWidget(self.GPU, 2, 1)
14178
15279
 
14179
15280
  misc_group.setLayout(misc_layout)
14180
15281
  main_layout.addWidget(misc_group)
@@ -14184,7 +15285,7 @@ class BranchDialog(QDialog):
14184
15285
  run_button.clicked.connect(self.branch_label)
14185
15286
  main_layout.addWidget(run_button)
14186
15287
 
14187
- if self.parent().channel_data[0] is not None or self.parent().channel_data[3] is not None:
15288
+ if (self.parent().channel_data[0] is not None or self.parent().channel_data[3] is not None) and not tutorial_example:
14188
15289
  QMessageBox.critical(
14189
15290
  self,
14190
15291
  "Alert",
@@ -14206,8 +15307,11 @@ class BranchDialog(QDialog):
14206
15307
  fix = self.fix.isChecked()
14207
15308
  fix2 = self.fix2.isChecked()
14208
15309
  fix3 = self.fix3.isChecked()
15310
+ fix4 = self.fix4.isChecked()
14209
15311
  fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
15312
+ fix4_val = float(self.fix4_val.text()) if self.fix4_val.text() else 10
14210
15313
  seed = int(self.seed.text()) if self.seed.text() else None
15314
+ compute = self.compute.isChecked()
14211
15315
 
14212
15316
  if my_network.edges is None and my_network.nodes is not None:
14213
15317
  self.parent().load_channel(1, my_network.nodes, data = True)
@@ -14224,7 +15328,12 @@ class BranchDialog(QDialog):
14224
15328
 
14225
15329
  if my_network.edges is not None and my_network.nodes is not None and my_network.id_overlay is not None:
14226
15330
 
14227
- output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
15331
+ if fix4:
15332
+ unify = True
15333
+ else:
15334
+ unify = False
15335
+
15336
+ output, verts, skeleton, endpoints = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape, compute = compute, unify = unify, union_val = fix4_val, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
14228
15337
 
14229
15338
  if fix2:
14230
15339
 
@@ -14261,7 +15370,23 @@ class BranchDialog(QDialog):
14261
15370
 
14262
15371
  if fix3:
14263
15372
 
14264
- output = self.parent().separate_nontouching_objects(output, max_val=np.max(output))
15373
+ output = self.parent().separate_nontouching_objects(output, max_val=np.max(output), branches = True)
15374
+
15375
+ if compute:
15376
+ if skeleton.shape != output.shape:
15377
+ print("Since downsampling was applied, skipping branchstats. Please use 'Analyze -> Stats -> Calculate Branch Stats' after this to find branch stats.")
15378
+ else:
15379
+ labeled_image = (skeleton != 0) * output
15380
+ len_dict, tortuosity_dict, angle_dict = n3d.compute_optional_branchstats(verts, labeled_image, endpoints, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
15381
+ self.parent().branch_dict[1] = [len_dict, tortuosity_dict]
15382
+ #max_length = max(len(v) for v in angle_dict.values())
15383
+ #title = [str(i+1) if i < 2 else i+1 for i in range(max_length)]
15384
+
15385
+ #del labeled_image
15386
+
15387
+ self.parent().format_for_upperright_table(len_dict, 'BranchID', 'Length (Scaled)', 'Branch Lengths')
15388
+ self.parent().format_for_upperright_table(tortuosity_dict, 'BranchID', 'Tortuosity', 'Branch Tortuosities')
15389
+ #self.parent().format_for_upperright_table(angle_dict, 'Vertex ID', title, 'Branch Angles')
14265
15390
 
14266
15391
 
14267
15392
  if down_factor is not None:
@@ -14437,7 +15562,7 @@ class ModifyDialog(QDialog):
14437
15562
  def __init__(self, parent=None):
14438
15563
  super().__init__(parent)
14439
15564
  self.setWindowTitle("Modify Network Qualities")
14440
- self.setModal(True)
15565
+ self.setModal(False)
14441
15566
  layout = QFormLayout(self)
14442
15567
 
14443
15568
  self.revid = QPushButton("Remove Unassigned")
@@ -14662,10 +15787,6 @@ class CentroidDialog(QDialog):
14662
15787
 
14663
15788
  layout = QFormLayout(self)
14664
15789
 
14665
- self.directory = QLineEdit()
14666
- self.directory.setPlaceholderText("Leave empty for active directory")
14667
- layout.addRow("Output Directory:", self.directory)
14668
-
14669
15790
  self.downsample = QLineEdit("1")
14670
15791
  layout.addRow("Downsample Factor:", self.downsample)
14671
15792
 
@@ -14695,7 +15816,7 @@ class CentroidDialog(QDialog):
14695
15816
  ignore_empty = self.ignore_empty.isChecked()
14696
15817
 
14697
15818
  # Get directory (None if empty)
14698
- directory = self.directory.text() if self.directory.text() else None
15819
+ directory = None
14699
15820
 
14700
15821
  # Get downsample
14701
15822
  try:
@@ -14776,7 +15897,6 @@ class CentroidDialog(QDialog):
14776
15897
 
14777
15898
  class CalcAllDialog(QDialog):
14778
15899
  # Class variables to store previous settings
14779
- prev_directory = ""
14780
15900
  prev_search = ""
14781
15901
  prev_diledge = ""
14782
15902
  prev_down_factor = ""
@@ -14793,7 +15913,7 @@ class CalcAllDialog(QDialog):
14793
15913
  def __init__(self, parent=None):
14794
15914
  super().__init__(parent)
14795
15915
  self.setWindowTitle("Calculate Connectivity Network Parameters")
14796
- self.setModal(True)
15916
+ self.setModal(False)
14797
15917
 
14798
15918
  # Main layout
14799
15919
  main_layout = QVBoxLayout(self)
@@ -14829,7 +15949,7 @@ class CalcAllDialog(QDialog):
14829
15949
 
14830
15950
  self.other_nodes = QLineEdit(self.prev_other_nodes)
14831
15951
  self.other_nodes.setPlaceholderText("Leave empty for None")
14832
- optional_layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
15952
+ #optional_layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
14833
15953
 
14834
15954
  self.remove_trunk = QLineEdit(self.prev_remove_trunk)
14835
15955
  self.remove_trunk.setPlaceholderText("Leave empty for 0")
@@ -14848,21 +15968,21 @@ class CalcAllDialog(QDialog):
14848
15968
 
14849
15969
  self.down_factor = QLineEdit(self.prev_down_factor)
14850
15970
  self.down_factor.setPlaceholderText("Leave empty for None")
14851
- speedup_layout.addRow("Downsample for Centroids (int):", self.down_factor)
15971
+ speedup_layout.addRow("Downsample for Centroids/Overlays (int):", self.down_factor)
14852
15972
 
14853
15973
  self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
14854
15974
  self.GPU_downsample.setPlaceholderText("Leave empty for None")
14855
- speedup_layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
15975
+ #speedup_layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
14856
15976
 
14857
15977
  self.gpu = QPushButton("GPU")
14858
15978
  self.gpu.setCheckable(True)
14859
15979
  self.gpu.setChecked(self.prev_gpu)
14860
- speedup_layout.addRow("Use GPU:", self.gpu)
15980
+ #speedup_layout.addRow("Use GPU:", self.gpu)
14861
15981
 
14862
15982
  self.fastdil = QPushButton("Fast Dilate")
14863
15983
  self.fastdil.setCheckable(True)
14864
15984
  self.fastdil.setChecked(self.prev_fastdil)
14865
- speedup_layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
15985
+ #speedup_layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
14866
15986
 
14867
15987
  main_layout.addWidget(speedup_group)
14868
15988
 
@@ -14870,10 +15990,6 @@ class CalcAllDialog(QDialog):
14870
15990
  output_group = QGroupBox("Output Options")
14871
15991
  output_layout = QFormLayout(output_group)
14872
15992
 
14873
- self.directory = QLineEdit(self.prev_directory)
14874
- self.directory.setPlaceholderText("Will Have to Save Manually If Empty")
14875
- output_layout.addRow("Output Directory:", self.directory)
14876
-
14877
15993
  self.overlays = QPushButton("Overlays")
14878
15994
  self.overlays.setCheckable(True)
14879
15995
  self.overlays.setChecked(self.prev_overlays)
@@ -14895,7 +16011,7 @@ class CalcAllDialog(QDialog):
14895
16011
 
14896
16012
  try:
14897
16013
  # Get directory (None if empty)
14898
- directory = self.directory.text() if self.directory.text() else None
16014
+ directory = None
14899
16015
 
14900
16016
  # Get xy_scale and z_scale (1 if empty or invalid)
14901
16017
  try:
@@ -14972,7 +16088,6 @@ class CalcAllDialog(QDialog):
14972
16088
  )
14973
16089
 
14974
16090
  # Store current values as previous values
14975
- CalcAllDialog.prev_directory = self.directory.text()
14976
16091
  CalcAllDialog.prev_search = self.search.text()
14977
16092
  CalcAllDialog.prev_diledge = self.diledge.text()
14978
16093
  CalcAllDialog.prev_down_factor = self.down_factor.text()
@@ -15006,8 +16121,12 @@ class CalcAllDialog(QDialog):
15006
16121
  directory = 'my_network'
15007
16122
 
15008
16123
  # Generate and update overlays
15009
- my_network.network_overlay = my_network.draw_network(directory=directory)
15010
- my_network.id_overlay = my_network.draw_node_indices(directory=directory)
16124
+ my_network.network_overlay = my_network.draw_network(directory=directory, down_factor = down_factor)
16125
+ my_network.id_overlay = my_network.draw_node_indices(directory=directory, down_factor = down_factor)
16126
+
16127
+ if down_factor is not None:
16128
+ my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
16129
+ my_network.network_overlay = n3d.upsample_with_padding(my_network.network_overlay, original_shape = self.parent().shape)
15011
16130
 
15012
16131
  # Update channel data
15013
16132
  self.parent().load_channel(2, my_network.network_overlay, True)
@@ -15072,10 +16191,10 @@ class CalcAllDialog(QDialog):
15072
16191
 
15073
16192
 
15074
16193
  class ProxDialog(QDialog):
15075
- def __init__(self, parent=None):
16194
+ def __init__(self, parent=None, tutorial_example = False):
15076
16195
  super().__init__(parent)
15077
16196
  self.setWindowTitle("Calculate Proximity Network")
15078
- self.setModal(True)
16197
+ self.setModal(False)
15079
16198
 
15080
16199
  # Main layout
15081
16200
  main_layout = QVBoxLayout(self)
@@ -15111,6 +16230,11 @@ class ProxDialog(QDialog):
15111
16230
  self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
15112
16231
  self.id_selector.setCurrentIndex(0) # Default to Mode 1
15113
16232
  mode_layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
16233
+ elif tutorial_example:
16234
+ self.id_selector = QComboBox()
16235
+ self.id_selector.addItems(['None'] + ['Example Identity A', 'Example Identity B', 'Example Identity C', 'etc...'])
16236
+ self.id_selector.setCurrentIndex(0) # Default to Mode 1
16237
+ mode_layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
15114
16238
  else:
15115
16239
  self.id_selector = None
15116
16240
 
@@ -15120,14 +16244,13 @@ class ProxDialog(QDialog):
15120
16244
  output_group = QGroupBox("Output Options")
15121
16245
  output_layout = QFormLayout(output_group)
15122
16246
 
15123
- self.directory = QLineEdit('')
15124
- self.directory.setPlaceholderText("Leave empty for 'my_network'")
15125
- output_layout.addRow("Output Directory:", self.directory)
15126
-
15127
16247
  self.overlays = QPushButton("Overlays")
15128
16248
  self.overlays.setCheckable(True)
15129
16249
  self.overlays.setChecked(True)
15130
16250
  output_layout.addRow("Generate Overlays:", self.overlays)
16251
+
16252
+ self.downsample = QLineEdit()
16253
+ output_layout.addRow("(If above): Downsample factor for drawing overlays (Int - Makes Overlay Elements Larger):", self.downsample)
15131
16254
 
15132
16255
  self.populate = QPushButton("Populate Nodes from Centroids?")
15133
16256
  self.populate.setCheckable(True)
@@ -15146,7 +16269,7 @@ class ProxDialog(QDialog):
15146
16269
  self.fastdil = QPushButton("Fast Dilate")
15147
16270
  self.fastdil.setCheckable(True)
15148
16271
  self.fastdil.setChecked(False)
15149
- speedup_layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
16272
+ #speedup_layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
15150
16273
 
15151
16274
  main_layout.addWidget(speedup_group)
15152
16275
 
@@ -15172,10 +16295,8 @@ class ProxDialog(QDialog):
15172
16295
  else:
15173
16296
  targets = None
15174
16297
 
15175
- try:
15176
- directory = self.directory.text() if self.directory.text() else None
15177
- except:
15178
- directory = None
16298
+ directory = None
16299
+
15179
16300
 
15180
16301
  # Get xy_scale and z_scale (1 if empty or invalid)
15181
16302
  try:
@@ -15199,6 +16320,12 @@ class ProxDialog(QDialog):
15199
16320
  except:
15200
16321
  max_neighbors = None
15201
16322
 
16323
+
16324
+ try:
16325
+ downsample = int(self.downsample.text()) if self.downsample.text() else None
16326
+ except:
16327
+ downsample = None
16328
+
15202
16329
  overlays = self.overlays.isChecked()
15203
16330
  fastdil = self.fastdil.isChecked()
15204
16331
 
@@ -15254,8 +16381,12 @@ class ProxDialog(QDialog):
15254
16381
  directory = 'my_network'
15255
16382
 
15256
16383
  # Generate and update overlays
15257
- my_network.network_overlay = my_network.draw_network(directory=directory)
15258
- my_network.id_overlay = my_network.draw_node_indices(directory=directory)
16384
+ my_network.network_overlay = my_network.draw_network(directory=directory, down_factor = downsample)
16385
+ my_network.id_overlay = my_network.draw_node_indices(directory=directory, down_factor = downsample)
16386
+
16387
+ if downsample is not None:
16388
+ my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
16389
+ my_network.network_overlay = n3d.upsample_with_padding(my_network.network_overlay, original_shape = self.parent().shape)
15259
16390
 
15260
16391
  # Update channel data
15261
16392
  self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True)
@@ -15915,7 +17046,186 @@ class HistogramSelector(QWidget):
15915
17046
  except Exception as e:
15916
17047
  print(f"Error generating dispersion histogram: {e}")
15917
17048
 
17049
+ class TutorialSelectionDialog(QWidget):
17050
+ """Dialog for selecting which tutorial to run"""
17051
+
17052
+ def __init__(self, window, parent=None):
17053
+ super().__init__(parent)
17054
+ self.window = window
17055
+ self.setWindowTitle("NetTracer3D Tutorials")
17056
+ self.setWindowFlags(Qt.WindowType.Window)
17057
+ self.setGeometry(200, 200, 400, 300)
17058
+
17059
+ layout = QVBoxLayout(self)
17060
+
17061
+ # Title
17062
+ title = QLabel("Select a Tutorial")
17063
+ title_font = QFont("Arial", 16, QFont.Weight.Bold)
17064
+ title.setFont(title_font)
17065
+ title.setAlignment(Qt.AlignmentFlag.AlignCenter)
17066
+ layout.addWidget(title)
17067
+
17068
+ # Description
17069
+ desc = QLabel("Choose a tutorial to learn about different features of NetTracer3D:")
17070
+ desc.setWordWrap(True)
17071
+ desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
17072
+ layout.addWidget(desc)
17073
+
17074
+ layout.addSpacing(20)
17075
+
17076
+ # Tutorial buttons
17077
+
17078
+ intro_btn = QPushButton("Intro")
17079
+ intro_btn.setMinimumHeight(50)
17080
+ intro_btn.clicked.connect(self.start_intro)
17081
+ layout.addWidget(intro_btn)
17082
+
17083
+ basics_btn = QPushButton("Basic Interface Tour")
17084
+ basics_btn.setMinimumHeight(50)
17085
+ basics_btn.clicked.connect(self.start_basics_tutorial)
17086
+ layout.addWidget(basics_btn)
17087
+
17088
+ image_btn = QPushButton("Visualization Control Overview")
17089
+ image_btn.setMinimumHeight(50)
17090
+ image_btn.clicked.connect(self.start_image_tutorial)
17091
+ layout.addWidget(image_btn)
17092
+
17093
+ file_btn = QPushButton("Saving/Loading Data and Assigning Node Identities")
17094
+ file_btn.setMinimumHeight(50)
17095
+ file_btn.clicked.connect(self.start_file)
17096
+ layout.addWidget(file_btn)
17097
+
17098
+ seg_btn = QPushButton("Segmenting Data")
17099
+ seg_btn.setMinimumHeight(50)
17100
+ seg_btn.clicked.connect(self.start_segment)
17101
+ layout.addWidget(seg_btn)
17102
+
17103
+ con_btn = QPushButton("1. Creating 'Connectivity Networks'")
17104
+ con_btn.setMinimumHeight(50)
17105
+ con_btn.clicked.connect(self.start_connectivity)
17106
+ layout.addWidget(con_btn)
17107
+
17108
+ branch_btn = QPushButton("2. Creating 'Branch Networks'")
17109
+ branch_btn.setMinimumHeight(50)
17110
+ branch_btn.clicked.connect(self.start_branch)
17111
+ layout.addWidget(branch_btn)
17112
+
17113
+ prox_btn = QPushButton("3. Creating 'Proximity Networks'")
17114
+ prox_btn.setMinimumHeight(50)
17115
+ prox_btn.clicked.connect(self.start_prox)
17116
+ layout.addWidget(prox_btn)
17117
+
17118
+ analysis_btn = QPushButton("Network and Image Analysis")
17119
+ analysis_btn.setMinimumHeight(50)
17120
+ analysis_btn.clicked.connect(self.start_analysis)
17121
+ layout.addWidget(analysis_btn)
17122
+
17123
+ processing_btn = QPushButton("Image Processing")
17124
+ processing_btn.setMinimumHeight(50)
17125
+ processing_btn.clicked.connect(self.start_process_tutorial)
17126
+ layout.addWidget(processing_btn)
17127
+
17128
+ layout.addStretch()
17129
+
17130
+ # Close button
17131
+ close_btn = QPushButton("Close")
17132
+ close_btn.clicked.connect(self.close)
17133
+ layout.addWidget(close_btn)
17134
+
17135
+ def start_intro(self):
17136
+ """Start the basic interface tutorial"""
17137
+ self.close()
17138
+ from . import tutorial
17139
+
17140
+ if not hasattr(self.window, 'start_tutorial_manager'):
17141
+ self.window.start_tutorial_manager = tutorial.setup_start_tutorial(self.window)
17142
+
17143
+ self.window.start_tutorial_manager.start()
17144
+
17145
+ def start_basics_tutorial(self):
17146
+ """Start the basic interface tutorial"""
17147
+ self.close()
17148
+ from . import tutorial
17149
+
17150
+ if not hasattr(self.window, 'basics_tutorial_manager'):
17151
+ self.window.basics_tutorial_manager = tutorial.setup_basics_tutorial(self.window)
17152
+
17153
+ self.window.basics_tutorial_manager.start()
17154
+
17155
+ def start_file(self):
17156
+ """Start the basic interface tutorial"""
17157
+ self.close()
17158
+ from . import tutorial
17159
+
17160
+ if not hasattr(self.window, 'file_tutorial_manager'):
17161
+ self.window.file_tutorial_manager = tutorial.setup_file_tutorial(self.window)
17162
+
17163
+ self.window.file_tutorial_manager.start()
17164
+
17165
+
17166
+ def start_segment(self):
17167
+ self.close()
17168
+ from . import tutorial
17169
+
17170
+ if not hasattr(self.window, 'seg_tutorial_manager'):
17171
+ self.window.seg_tutorial_manager = tutorial.setup_seg_tutorial(self.window)
17172
+ self.window.seg_tutorial_manager.start()
17173
+
17174
+ def start_connectivity(self):
17175
+ self.close()
17176
+ from . import tutorial
17177
+
17178
+ if not hasattr(self.window, 'connectivity_tutorial_manager'):
17179
+ self.window.connectivity_tutorial_manager = tutorial.setup_connectivity_tutorial(self.window)
17180
+
17181
+ self.window.connectivity_tutorial_manager.start()
17182
+
17183
+ def start_branch(self):
17184
+ self.close()
17185
+ from . import tutorial
17186
+
17187
+ if not hasattr(self.window, 'branch_tutorial_manager'):
17188
+ self.window.branch_tutorial_manager = tutorial.setup_branch_tutorial(self.window)
17189
+
17190
+ self.window.branch_tutorial_manager.start()
17191
+
17192
+ def start_prox(self):
17193
+ self.close()
17194
+ from . import tutorial
17195
+
17196
+ if not hasattr(self.window, 'prox_tutorial_manager'):
17197
+ self.window.prox_tutorial_manager = tutorial.setup_prox_tutorial(self.window)
17198
+
17199
+ self.window.prox_tutorial_manager.start()
17200
+
17201
+ def start_analysis(self):
17202
+ self.close()
17203
+ from . import tutorial
17204
+
17205
+ if not hasattr(self.window, 'analysis_tutorial_manager'):
17206
+ self.window.analysis_tutorial_manager = tutorial.setup_analysis_tutorial(self.window)
17207
+
17208
+ self.window.analysis_tutorial_manager.start()
17209
+
17210
+ def start_process_tutorial(self):
17211
+ """Start the image processing tutorial"""
17212
+ self.close()
17213
+ from . import tutorial
17214
+
17215
+ if not hasattr(self.window, 'process_tutorial_manager'):
17216
+ self.window.process_tutorial_manager = tutorial.setup_process_tutorial(self.window)
17217
+
17218
+ self.window.process_tutorial_manager.start()
15918
17219
 
17220
+ def start_image_tutorial(self):
17221
+ """Start the image tutorial"""
17222
+ self.close()
17223
+ from . import tutorial
17224
+
17225
+ if not hasattr(self.window, 'image_tutorial_manager'):
17226
+ self.window.image_tutorial_manager = tutorial.setup_image_tutorial(self.window)
17227
+
17228
+ self.window.image_tutorial_manager.start()
15919
17229
 
15920
17230
  # Initiating this program from the script line:
15921
17231