nettracer3d 1.1.1__py3-none-any.whl → 1.2.4__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
 
@@ -4894,6 +5189,14 @@ class ImageViewerWindow(QMainWindow):
4894
5189
  print(f"Error opening URL: {e}")
4895
5190
  return False
4896
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
+
4897
5200
 
4898
5201
  def stats(self):
4899
5202
  """Method to get and display the network stats"""
@@ -4962,7 +5265,7 @@ class ImageViewerWindow(QMainWindow):
4962
5265
 
4963
5266
 
4964
5267
 
4965
- 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):
4966
5269
  """
4967
5270
  Format dictionary or list data for display in upper right table.
4968
5271
 
@@ -5058,15 +5361,16 @@ class ImageViewerWindow(QMainWindow):
5058
5361
  # Add to tabbed widget
5059
5362
  if title is None:
5060
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
5061
5366
  else:
5062
5367
  self.tabbed_data.add_table(f"{title}", table)
5063
-
5064
-
5065
-
5066
5368
  # Adjust column widths to content
5067
5369
  for column in range(table.model().columnCount(None)):
5068
5370
  table.resizeColumnToContents(column)
5069
5371
 
5372
+ if save:
5373
+ table.save_table_as('csv')
5070
5374
  return df
5071
5375
 
5072
5376
  except:
@@ -5117,12 +5421,15 @@ class ImageViewerWindow(QMainWindow):
5117
5421
  def show_calc_all_dialog(self):
5118
5422
  """Show the calculate all parameter dialog."""
5119
5423
  dialog = CalcAllDialog(self)
5120
- dialog.exec()
5424
+ dialog.show()
5121
5425
 
5122
- def show_calc_prox_dialog(self):
5426
+ def show_calc_prox_dialog(self, tutorial_example = False):
5123
5427
  """Show the proximity calc dialog"""
5124
- dialog = ProxDialog(self)
5125
- dialog.exec()
5428
+ dialog = ProxDialog(self, tutorial_example = True)
5429
+ if tutorial_example:
5430
+ dialog.show()
5431
+ else:
5432
+ dialog.exec()
5126
5433
 
5127
5434
  def table_load_attrs(self):
5128
5435
 
@@ -5185,8 +5492,6 @@ class ImageViewerWindow(QMainWindow):
5185
5492
  self.load_channel(1, my_network.nodes, data = True)
5186
5493
  self.delete_channel(0, False)
5187
5494
 
5188
- my_network.id_overlay = my_network.edges.copy()
5189
-
5190
5495
  self.show_gennodes_dialog()
5191
5496
 
5192
5497
  my_network.edges = (my_network.nodes == 0) * my_network.edges
@@ -5195,7 +5500,6 @@ class ImageViewerWindow(QMainWindow):
5195
5500
 
5196
5501
  self.load_channel(1, my_network.edges, data = True)
5197
5502
  self.load_channel(0, my_network.nodes, data = True)
5198
- self.load_channel(3, my_network.id_overlay, data = True)
5199
5503
 
5200
5504
  self.table_load_attrs()
5201
5505
 
@@ -5225,6 +5529,12 @@ class ImageViewerWindow(QMainWindow):
5225
5529
 
5226
5530
  self.load_channel(0, my_network.edges, data = True)
5227
5531
 
5532
+ try:
5533
+ self.branch_dict[0] = self.branch_dict[1]
5534
+ self.branch_dict[1] = None
5535
+ except:
5536
+ pass
5537
+
5228
5538
  self.delete_channel(1, False)
5229
5539
 
5230
5540
  my_network.morph_proximity(search = [3,3], fastdil = True)
@@ -5241,14 +5551,51 @@ class ImageViewerWindow(QMainWindow):
5241
5551
  dialog = CentroidDialog(self)
5242
5552
  dialog.exec()
5243
5553
 
5244
- 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):
5245
5592
  """show the dilate dialog"""
5246
- dialog = DilateDialog(self)
5247
- dialog.exec()
5593
+ dialog = DilateDialog(self, args)
5594
+ dialog.show()
5248
5595
 
5249
- def show_erode_dialog(self):
5596
+ def show_erode_dialog(self, args = None):
5250
5597
  """show the erode dialog"""
5251
- dialog = ErodeDialog(self)
5598
+ dialog = ErodeDialog(self, args)
5252
5599
  dialog.exec()
5253
5600
 
5254
5601
  def show_hole_dialog(self):
@@ -5256,6 +5603,11 @@ class ImageViewerWindow(QMainWindow):
5256
5603
  dialog = HoleDialog(self)
5257
5604
  dialog.exec()
5258
5605
 
5606
+ def show_filament_dialog(self):
5607
+ """show the filament dialog"""
5608
+ dialog = FilamentDialog(self)
5609
+ dialog.show()
5610
+
5259
5611
  def show_label_dialog(self):
5260
5612
  """Show the label dialog"""
5261
5613
  dialog = LabelDialog(self)
@@ -5266,13 +5618,20 @@ class ImageViewerWindow(QMainWindow):
5266
5618
  dialog = SLabelDialog(self)
5267
5619
  dialog.exec()
5268
5620
 
5269
- def show_thresh_dialog(self):
5621
+ def show_thresh_dialog(self, tutorial_example = False):
5270
5622
  """Show threshold dialog"""
5271
5623
  if self.machine_window is not None:
5272
5624
  return
5273
5625
 
5274
5626
  dialog = ThresholdDialog(self)
5275
- 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()
5276
5635
 
5277
5636
 
5278
5637
  def show_mask_dialog(self):
@@ -5309,15 +5668,21 @@ class ImageViewerWindow(QMainWindow):
5309
5668
  dialog.exec()
5310
5669
 
5311
5670
 
5312
- def show_gennodes_dialog(self, down_factor = None, called = False):
5671
+ def show_gennodes_dialog(self, down_factor = None, called = False, tutorial_example = False):
5313
5672
  """show the gennodes dialog"""
5314
5673
  gennodes = GenNodesDialog(self, down_factor = down_factor, called = called)
5315
- gennodes.exec()
5674
+ if not tutorial_example:
5675
+ gennodes.exec()
5676
+ else:
5677
+ gennodes.show()
5316
5678
 
5317
- def show_branch_dialog(self, called = False):
5679
+ def show_branch_dialog(self, called = False, tutorial_example = False):
5318
5680
  """Show the branch label dialog"""
5319
- dialog = BranchDialog(self, called = called)
5320
- dialog.exec()
5681
+ dialog = BranchDialog(self, called = called, tutorial_example = tutorial_example)
5682
+ if tutorial_example:
5683
+ dialog.show()
5684
+ else:
5685
+ dialog.exec()
5321
5686
 
5322
5687
  def voronoi(self):
5323
5688
 
@@ -5333,7 +5698,7 @@ class ImageViewerWindow(QMainWindow):
5333
5698
  def show_modify_dialog(self):
5334
5699
  """Show the network modify dialog"""
5335
5700
  dialog = ModifyDialog(self)
5336
- dialog.exec()
5701
+ dialog.show()
5337
5702
 
5338
5703
 
5339
5704
  def show_binarize_dialog(self):
@@ -5347,11 +5712,14 @@ class ImageViewerWindow(QMainWindow):
5347
5712
  dialog = ResizeDialog(self)
5348
5713
  dialog.exec()
5349
5714
 
5715
+ def show_clean_dialog(self):
5716
+ dialog = CleanDialog(self)
5717
+ dialog.show()
5350
5718
 
5351
5719
  def show_properties_dialog(self):
5352
5720
  """Show the properties dialog"""
5353
5721
  dialog = PropertiesDialog(self)
5354
- dialog.exec()
5722
+ dialog.show()
5355
5723
 
5356
5724
  def show_brightness_dialog(self):
5357
5725
  """Show the brightness/contrast control dialog."""
@@ -5963,7 +6331,7 @@ class ImageViewerWindow(QMainWindow):
5963
6331
  msg = QMessageBox()
5964
6332
  msg.setIcon(QMessageBox.Icon.Question)
5965
6333
  msg.setText("Image Format Alert")
5966
- 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.")
5967
6335
  msg.setWindowTitle("Resize")
5968
6336
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
5969
6337
  return msg.exec() == QMessageBox.StandardButton.Yes
@@ -5990,11 +6358,13 @@ class ImageViewerWindow(QMainWindow):
5990
6358
  if 'YResolution' in tags:
5991
6359
  y_res = tags['YResolution'].value
5992
6360
  y_scale = y_res[1] / y_res[0] if isinstance(y_res, tuple) else 1.0 / y_res
5993
-
5994
- if x_scale is None:
6361
+
6362
+ if x_scale == None:
5995
6363
  x_scale = 1
5996
- if z_scale is None:
6364
+ if z_scale == None:
5997
6365
  z_scale = 1
6366
+ if x_scale == 1 and z_scale == 1:
6367
+ return
5998
6368
 
5999
6369
  return x_scale, z_scale
6000
6370
 
@@ -6029,7 +6399,11 @@ class ImageViewerWindow(QMainWindow):
6029
6399
  print(f"xy_scale property set to {my_network.xy_scale}; z_scale property set to {my_network.z_scale}")
6030
6400
  except:
6031
6401
  pass
6032
- 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
6033
6407
 
6034
6408
  elif file_extension == 'nii':
6035
6409
  import nibabel as nib
@@ -6107,7 +6481,6 @@ class ImageViewerWindow(QMainWindow):
6107
6481
  if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
6108
6482
  try:
6109
6483
  if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
6110
- self.resizing = True
6111
6484
  reset_resize = True
6112
6485
  self.highlight_overlay = None
6113
6486
  except:
@@ -6122,6 +6495,8 @@ class ImageViewerWindow(QMainWindow):
6122
6495
  if self.confirm_resize_dialog():
6123
6496
  self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
6124
6497
  break
6498
+ else:
6499
+ return
6125
6500
 
6126
6501
  if not begin_paint:
6127
6502
  if channel_index == 0:
@@ -6194,7 +6569,13 @@ class ImageViewerWindow(QMainWindow):
6194
6569
 
6195
6570
  if self.shape == self.channel_data[channel_index].shape:
6196
6571
  preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
6197
- 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)
6198
6579
  if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
6199
6580
  self.throttle = True
6200
6581
  else:
@@ -6202,7 +6583,6 @@ class ImageViewerWindow(QMainWindow):
6202
6583
 
6203
6584
 
6204
6585
  self.img_height, self.img_width = self.shape[1], self.shape[2]
6205
- self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
6206
6586
 
6207
6587
  self.completed_paint_strokes = [] #Reset pending paint operations
6208
6588
  self.current_stroke_points = []
@@ -6413,6 +6793,8 @@ class ImageViewerWindow(QMainWindow):
6413
6793
  #print(f"Saved {self.channel_names[ch_index]}" + (f" to: {filename}" if filename else "")) # Debug print
6414
6794
 
6415
6795
  except Exception as e:
6796
+ import traceback
6797
+ traceback.print_exc()
6416
6798
  QMessageBox.critical(
6417
6799
  self,
6418
6800
  "Error Saving File",
@@ -6436,12 +6818,9 @@ class ImageViewerWindow(QMainWindow):
6436
6818
  def update_slice(self):
6437
6819
  """Queue a slice update when slider moves."""
6438
6820
  # Store current view settings
6439
- if not self.resizing:
6440
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
6441
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
6442
- else:
6443
- current_xlim = None
6444
- 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
+
6445
6824
 
6446
6825
  # Store the pending slice and view settings
6447
6826
  self.pending_slice = (self.slice_slider.value(), (current_xlim, current_ylim))
@@ -6469,6 +6848,11 @@ class ImageViewerWindow(QMainWindow):
6469
6848
  self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
6470
6849
  elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
6471
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
6472
6856
  self.update_display(preserve_zoom=view_settings)
6473
6857
  if self.pan_mode:
6474
6858
  self.pan_button.click()
@@ -6826,6 +7210,12 @@ class ImageViewerWindow(QMainWindow):
6826
7210
  if current_xlim is not None and current_ylim is not None:
6827
7211
  self.ax.set_xlim(current_xlim)
6828
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
+
6829
7219
 
6830
7220
  if reset_resize:
6831
7221
  self.resizing = False
@@ -6836,8 +7226,6 @@ class ImageViewerWindow(QMainWindow):
6836
7226
 
6837
7227
  except Exception as e:
6838
7228
  pass
6839
- #import traceback
6840
- #print(traceback.format_exc())
6841
7229
 
6842
7230
 
6843
7231
  def get_channel_image(self, channel):
@@ -6947,12 +7335,73 @@ class ImageViewerWindow(QMainWindow):
6947
7335
  dialog = RadDialog(self)
6948
7336
  dialog.exec()
6949
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
+
6950
7399
  def show_interaction_dialog(self):
6951
7400
  dialog = InteractionDialog(self)
6952
7401
  dialog.exec()
6953
7402
 
6954
- def show_violin_dialog(self):
6955
- dialog = ViolinDialog(self)
7403
+ def show_violin_dialog(self, called = False):
7404
+ dialog = ViolinDialog(self, called = called)
6956
7405
  dialog.show()
6957
7406
 
6958
7407
  def show_degree_dialog(self):
@@ -7159,7 +7608,7 @@ class CustomTableView(QTableView):
7159
7608
  else: # Bottom tables
7160
7609
  # Add Find action
7161
7610
  find_menu = context_menu.addMenu("Find")
7162
- find_action = find_menu.addAction("Find Node/Edge")
7611
+ find_action = find_menu.addAction("Find Node/Edge/")
7163
7612
  find_pair_action = find_menu.addAction("Find Pair")
7164
7613
  find_action.triggered.connect(lambda: self.handle_find_action(
7165
7614
  index.row(), index.column(),
@@ -7752,7 +8201,7 @@ class PropertiesDialog(QDialog):
7752
8201
  def __init__(self, parent=None):
7753
8202
  super().__init__(parent)
7754
8203
  self.setWindowTitle("Properties")
7755
- self.setModal(True)
8204
+ self.setModal(False)
7756
8205
 
7757
8206
  layout = QFormLayout(self)
7758
8207
 
@@ -7805,9 +8254,9 @@ class PropertiesDialog(QDialog):
7805
8254
  run_button.clicked.connect(self.run_properties)
7806
8255
  layout.addWidget(run_button)
7807
8256
 
7808
- report_button = QPushButton("Report Properties (Show in Top Right Tables)")
7809
- report_button.clicked.connect(self.report)
7810
- 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)
7811
8260
 
7812
8261
  def check_checked(self, ques):
7813
8262
 
@@ -8288,11 +8737,6 @@ class MergeNodeIdDialog(QDialog):
8288
8737
  self.mode_selector.addItems(["Auto-Binarize(Otsu)/Presegmented", "Manual (Interactive Thresholder)"])
8289
8738
  self.mode_selector.setCurrentIndex(1) # Default to Mode 1
8290
8739
  layout.addRow("Binarization Strategy:", self.mode_selector)
8291
-
8292
- self.umap = QPushButton("If using manual threshold: Also generate identity UMAP?")
8293
- self.umap.setCheckable(True)
8294
- self.umap.setChecked(True)
8295
- layout.addWidget(self.umap)
8296
8740
 
8297
8741
  self.include = QPushButton("Include When a Node is Negative for an ID?")
8298
8742
  self.include.setCheckable(True)
@@ -8349,7 +8793,7 @@ class MergeNodeIdDialog(QDialog):
8349
8793
  z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
8350
8794
  data = self.parent().channel_data[0]
8351
8795
  include = self.include.isChecked()
8352
- umap = self.umap.isChecked()
8796
+ umap = True
8353
8797
 
8354
8798
  if data is None:
8355
8799
  return
@@ -8484,18 +8928,23 @@ class MergeNodeIdDialog(QDialog):
8484
8928
  all_keys = id_dicts[0].keys()
8485
8929
  result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
8486
8930
 
8487
-
8488
- self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity (Save this Table for "Analyze -> Stats -> Show Violins")')
8489
- if umap:
8490
- my_network.identity_umap(result)
8491
-
8492
-
8493
8931
  QMessageBox.information(
8494
8932
  self,
8495
8933
  "Success",
8496
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)"
8497
8935
  )
8498
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
8499
8948
  self.accept()
8500
8949
  else:
8501
8950
  my_network.merge_node_ids(selected_path, data, include)
@@ -8614,7 +9063,7 @@ class Show3dDialog(QDialog):
8614
9063
  self.cubic = QPushButton("Cubic")
8615
9064
  self.cubic.setCheckable(True)
8616
9065
  self.cubic.setChecked(False)
8617
- 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)
8618
9067
 
8619
9068
  self.box = QPushButton("Box")
8620
9069
  self.box.setCheckable(True)
@@ -8665,6 +9114,9 @@ class Show3dDialog(QDialog):
8665
9114
  if visible:
8666
9115
  arrays_4d.append(channel)
8667
9116
 
9117
+ if self.parent().thresh_window_ref is not None:
9118
+ self.parent().thresh_window_ref.make_full_highlight()
9119
+
8668
9120
  if self.parent().highlight_overlay is not None or self.parent().mini_overlay_data is not None:
8669
9121
  if self.parent().mini_overlay == True:
8670
9122
  self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
@@ -8761,45 +9213,51 @@ class IdOverlayDialog(QDialog):
8761
9213
 
8762
9214
  def idoverlay(self):
8763
9215
 
8764
- accepted_mode = self.mode_selector.currentIndex()
8765
-
8766
9216
  try:
8767
- downsample = float(self.downsample.text()) if self.downsample.text() else None
8768
- except ValueError:
8769
- downsample = None
8770
9217
 
8771
- if accepted_mode == 0:
9218
+ accepted_mode = self.mode_selector.currentIndex()
8772
9219
 
8773
- 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
8774
9224
 
8775
- self.parent().show_centroid_dialog()
9225
+ if accepted_mode == 0:
8776
9226
 
8777
- if my_network.node_centroids is None:
8778
- return
9227
+ if my_network.node_centroids is None:
8779
9228
 
8780
- elif accepted_mode == 1:
9229
+ self.parent().show_centroid_dialog()
8781
9230
 
8782
- if my_network.edge_centroids is None:
9231
+ if my_network.node_centroids is None:
9232
+ return
8783
9233
 
8784
- self.parent().show_centroid_dialog()
9234
+ elif accepted_mode == 1:
8785
9235
 
8786
- if my_network.edge_centroids is None:
8787
- return
9236
+ if my_network.edge_centroids is None:
8788
9237
 
8789
- if accepted_mode == 0:
9238
+ self.parent().show_centroid_dialog()
8790
9239
 
8791
- my_network.id_overlay = my_network.draw_node_indices(down_factor = downsample)
9240
+ if my_network.edge_centroids is None:
9241
+ return
8792
9242
 
8793
- elif accepted_mode == 1:
9243
+ if accepted_mode == 0:
8794
9244
 
8795
- my_network.id_overlay = my_network.draw_edge_indices(down_factor = downsample)
9245
+ my_network.id_overlay = my_network.draw_node_indices(down_factor = downsample)
8796
9246
 
8797
- if downsample is not None:
8798
- my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
9247
+ elif accepted_mode == 1:
8799
9248
 
8800
- 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()))
9249
+ my_network.id_overlay = my_network.draw_edge_indices(down_factor = downsample)
9250
+
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}")
8801
9260
 
8802
- self.accept()
8803
9261
 
8804
9262
  class ColorOverlayDialog(QDialog):
8805
9263
 
@@ -8975,11 +9433,6 @@ class NetShowDialog(QDialog):
8975
9433
  self.weighted.setCheckable(True)
8976
9434
  self.weighted.setChecked(True)
8977
9435
  layout.addRow("Use Weighted Network (Only for community graphs):", self.weighted)
8978
-
8979
- # Optional saving:
8980
- self.directory = QLineEdit()
8981
- self.directory.setPlaceholderText("Does not save when empty")
8982
- layout.addRow("Output Directory:", self.directory)
8983
9436
 
8984
9437
  # Add Run button
8985
9438
  run_button = QPushButton("Show Network")
@@ -8995,7 +9448,7 @@ class NetShowDialog(QDialog):
8995
9448
  self.parent().show_centroid_dialog()
8996
9449
  accepted_mode = self.mode_selector.currentIndex() # Convert to 1-based index
8997
9450
  # Get directory (None if empty)
8998
- directory = self.directory.text() if self.directory.text() else None
9451
+ directory = None
8999
9452
 
9000
9453
  weighted = self.weighted.isChecked()
9001
9454
 
@@ -9038,7 +9491,7 @@ class PartitionDialog(QDialog):
9038
9491
 
9039
9492
  # Add mode selection dropdown
9040
9493
  self.mode_selector = QComboBox()
9041
- self.mode_selector.addItems(["Label Propogation", "Louvain"])
9494
+ self.mode_selector.addItems(["Louvain", "Label Propogation"])
9042
9495
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
9043
9496
  layout.addRow("Execution Mode:", self.mode_selector)
9044
9497
 
@@ -9061,6 +9514,10 @@ class PartitionDialog(QDialog):
9061
9514
  self.parent().prev_coms = None
9062
9515
 
9063
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
9064
9521
  weighted = self.weighted.isChecked()
9065
9522
  dostats = self.stats.isChecked()
9066
9523
 
@@ -9337,9 +9794,6 @@ class RadialDialog(QDialog):
9337
9794
  self.distance = QLineEdit("50")
9338
9795
  layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
9339
9796
 
9340
- self.directory = QLineEdit("")
9341
- layout.addRow("Output Directory:", self.directory)
9342
-
9343
9797
  # Add Run button
9344
9798
  run_button = QPushButton("Get Radial Distribution")
9345
9799
  run_button.clicked.connect(self.radial)
@@ -9351,7 +9805,7 @@ class RadialDialog(QDialog):
9351
9805
 
9352
9806
  distance = float(self.distance.text()) if self.distance.text().strip() else 50
9353
9807
 
9354
- directory = str(self.distance.text()) if self.directory.text().strip() else None
9808
+ directory = None
9355
9809
 
9356
9810
  if my_network.node_centroids is None:
9357
9811
  self.parent().show_centroid_dialog()
@@ -9635,11 +10089,8 @@ class NeighborIdentityDialog(QDialog):
9635
10089
  else:
9636
10090
  self.root = None
9637
10091
 
9638
- self.directory = QLineEdit("")
9639
- layout.addRow("Output Directory:", self.directory)
9640
-
9641
10092
  self.mode = QComboBox()
9642
- self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
10093
+ self.mode.addItems(["From Network - Quantifies Neighbors Based on Adjacent Network Connections", "Use Labeled Nodes - Quantifies Neighbors Volume of Neighbor Within Search Region"])
9643
10094
  self.mode.setCurrentIndex(0)
9644
10095
  layout.addRow("Mode", self.mode)
9645
10096
 
@@ -9649,7 +10100,7 @@ class NeighborIdentityDialog(QDialog):
9649
10100
  self.fastdil = QPushButton("Fast Dilate")
9650
10101
  self.fastdil.setCheckable(True)
9651
10102
  self.fastdil.setChecked(False)
9652
- 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)
9653
10104
 
9654
10105
  # Add Run button
9655
10106
  run_button = QPushButton("Get Neighborhood Identity Distribution")
@@ -9665,7 +10116,7 @@ class NeighborIdentityDialog(QDialog):
9665
10116
  except:
9666
10117
  pass
9667
10118
 
9668
- directory = self.directory.text() if self.directory.text().strip() else None
10119
+ directory = None
9669
10120
 
9670
10121
  mode = self.mode.currentIndex()
9671
10122
 
@@ -10060,7 +10511,7 @@ class RadDialog(QDialog):
10060
10511
  self.GPU = QPushButton("GPU")
10061
10512
  self.GPU.setCheckable(True)
10062
10513
  self.GPU.setChecked(False)
10063
- layout.addRow("Use GPU:", self.GPU)
10514
+ #layout.addRow("Use GPU:", self.GPU)
10064
10515
 
10065
10516
 
10066
10517
  # Add Run button
@@ -10094,9 +10545,6 @@ class RadDialog(QDialog):
10094
10545
  print(f"Error: {e}")
10095
10546
 
10096
10547
 
10097
-
10098
-
10099
-
10100
10548
  class InteractionDialog(QDialog):
10101
10549
 
10102
10550
  def __init__(self, parent=None):
@@ -10138,7 +10586,7 @@ class InteractionDialog(QDialog):
10138
10586
  self.fastdil = QPushButton("Fast Dilate")
10139
10587
  self.fastdil.setCheckable(True)
10140
10588
  self.fastdil.setChecked(False)
10141
- 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)
10142
10590
 
10143
10591
  # Add Run button
10144
10592
  run_button = QPushButton("Calculate")
@@ -10181,34 +10629,40 @@ class InteractionDialog(QDialog):
10181
10629
 
10182
10630
  class ViolinDialog(QDialog):
10183
10631
 
10184
- def __init__(self, parent=None):
10185
-
10632
+ def __init__(self, parent=None, called = False):
10186
10633
  super().__init__(parent)
10187
-
10188
- QMessageBox.critical(
10189
- self,
10190
- "Notice",
10191
- "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.)"
10192
- )
10193
-
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
+ )
10194
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
10195
10655
  try:
10196
- 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)
10197
10659
  except:
10198
- return
10199
-
10200
- self.backup_df = copy.deepcopy(self.df)
10201
- # Get all identity lists and normalize the dataframe
10202
- identity_lists = self.get_all_identity_lists()
10203
- self.df = self.normalize_df_with_identity_centerpoints(self.df, identity_lists)
10204
-
10205
- self.setWindowTitle("Violin Parameters")
10660
+ pass
10661
+ self.setWindowTitle("Violin/Neighborhood Parameters")
10206
10662
  self.setModal(False)
10207
-
10208
10663
  layout = QFormLayout(self)
10209
-
10664
+
10210
10665
  if my_network.node_identities is not None:
10211
-
10212
10666
  self.idens = QComboBox()
10213
10667
  all_idens = list(set(my_network.node_identities.values()))
10214
10668
  idens = []
@@ -10230,16 +10684,49 @@ class ViolinDialog(QDialog):
10230
10684
  self.coms.addItems(coms)
10231
10685
  self.coms.setCurrentIndex(0)
10232
10686
  layout.addRow("Return Neighborhood/Community Violin Plots?", self.coms)
10233
-
10687
+
10234
10688
  # Add Run button
10235
10689
  run_button = QPushButton("Show Z-score-like Violin")
10236
10690
  run_button.clicked.connect(self.run)
10237
10691
  layout.addWidget(run_button)
10238
-
10692
+
10239
10693
  run_button2 = QPushButton("Show Z-score UMAP")
10240
10694
  run_button2.clicked.connect(self.run2)
10241
- 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
+
10242
10727
  except:
10728
+ import traceback
10729
+ print(traceback.format_exc())
10243
10730
  QTimer.singleShot(0, self.close)
10244
10731
 
10245
10732
  def get_all_identity_lists(self):
@@ -10281,6 +10768,63 @@ class ViolinDialog(QDialog):
10281
10768
 
10282
10769
  return identity_lists
10283
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
+
10284
10828
  def normalize_df_with_identity_centerpoints(self, df, identity_lists):
10285
10829
  """
10286
10830
  Normalize the entire dataframe using identity-specific centerpoints.
@@ -10312,7 +10856,7 @@ class ViolinDialog(QDialog):
10312
10856
  # Get nodes that exist in both the identity list and the dataframe
10313
10857
  valid_nodes = [node for node in node_list if node in df_copy.index]
10314
10858
  if valid_nodes and ((str(identity) == str(column)) or str(identity) == f'{str(column)}+'):
10315
- # Get the median value for this identity in this column
10859
+ # Get the min value for this identity in this column
10316
10860
  identity_min = df_copy.loc[valid_nodes, column].min()
10317
10861
  centerpoint = identity_min
10318
10862
  break # Found the match, no need to continue
@@ -10368,7 +10912,7 @@ class ViolinDialog(QDialog):
10368
10912
  for column in range(table.model().columnCount(None)):
10369
10913
  table.resizeColumnToContents(column)
10370
10914
 
10371
- def run(self):
10915
+ def run(self, com = None):
10372
10916
 
10373
10917
  def df_to_dict_by_rows(df, row_indices, title):
10374
10918
  """
@@ -10407,6 +10951,23 @@ class ViolinDialog(QDialog):
10407
10951
 
10408
10952
  from . import neighborhoods
10409
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
+
10410
10971
  try:
10411
10972
 
10412
10973
  if self.idens.currentIndex() != 0:
@@ -10446,31 +11007,166 @@ class ViolinDialog(QDialog):
10446
11007
  except:
10447
11008
  pass
10448
11009
 
10449
-
10450
11010
  def run2(self):
10451
- def df_to_dict(df):
10452
- # Make a copy to avoid modifying the original dataframe
10453
- df_copy = df.copy()
10454
-
10455
- # Set the first column as the index (row headers)
10456
- df_copy = df_copy.set_index(df_copy.columns[0])
10457
-
10458
- # Convert all remaining columns to float type (batch conversion)
10459
- df_copy = df_copy.astype(float)
10460
-
10461
- # Create the result dictionary
10462
- result_dict = {}
10463
- for row_idx in df_copy.index:
10464
- result_dict[row_idx] = df_copy.loc[row_idx].tolist()
10465
-
10466
- return result_dict
10467
11011
 
10468
11012
  try:
10469
- umap_dict = df_to_dict(self.backup_df)
10470
- 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)
11016
+ except:
11017
+ import traceback
11018
+ print(traceback.format_exc())
11019
+ pass
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()
10471
11111
  except:
11112
+ import traceback
11113
+ print(traceback.format_exc())
10472
11114
  pass
10473
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
+
10474
11170
 
10475
11171
 
10476
11172
 
@@ -10827,7 +11523,7 @@ class ResizeDialog(QDialog):
10827
11523
 
10828
11524
 
10829
11525
  # cubic checkbox (default False)
10830
- 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)")
10831
11527
  self.cubic.setCheckable(True)
10832
11528
  self.cubic.setChecked(False)
10833
11529
  layout.addRow("Use cubic algorithm:", self.cubic)
@@ -10850,7 +11546,7 @@ class ResizeDialog(QDialog):
10850
11546
 
10851
11547
  def run_resize(self, undo = False, upsize = True, special = False):
10852
11548
  try:
10853
- self.parent().resizing = False
11549
+ self.parent().resizing = True
10854
11550
  # Get parameters
10855
11551
  try:
10856
11552
  resize = float(self.resize.text()) if self.resize.text() else None
@@ -10965,6 +11661,7 @@ class ResizeDialog(QDialog):
10965
11661
  if channel is not None:
10966
11662
  self.parent().slice_slider.setMinimum(0)
10967
11663
  self.parent().slice_slider.setMaximum(channel.shape[0] - 1)
11664
+ self.parent().shape = channel.shape
10968
11665
  break
10969
11666
 
10970
11667
  if not special:
@@ -11034,10 +11731,7 @@ class ResizeDialog(QDialog):
11034
11731
  except Exception as e:
11035
11732
  print(f"Error loading edge centroid table: {e}")
11036
11733
 
11037
-
11038
11734
  self.parent().update_display()
11039
- self.reset_fields()
11040
- self.parent().resizing = False
11041
11735
  self.accept()
11042
11736
 
11043
11737
  except Exception as e:
@@ -11046,6 +11740,92 @@ class ResizeDialog(QDialog):
11046
11740
  print(traceback.format_exc())
11047
11741
  QMessageBox.critical(self, "Error", f"Failed to resize: {str(e)}")
11048
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
+
11049
11829
 
11050
11830
  class OverrideDialog(QDialog):
11051
11831
  def __init__(self, parent=None):
@@ -11200,11 +11980,7 @@ class BinarizeDialog(QDialog):
11200
11980
  )
11201
11981
 
11202
11982
  # Update both the display data and the network object
11203
- self.parent().channel_data[self.parent().active_channel] = result
11204
-
11205
-
11206
- # Update the corresponding property in my_network
11207
- setattr(my_network, network_properties[self.parent().active_channel], result)
11983
+ self.parent().load_channel(self.parent().active_channel, result, True)
11208
11984
 
11209
11985
  self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11210
11986
  self.accept()
@@ -11252,11 +12028,7 @@ class LabelDialog(QDialog):
11252
12028
  )
11253
12029
 
11254
12030
  # Update both the display data and the network object
11255
- self.parent().channel_data[self.parent().active_channel] = result
11256
-
11257
-
11258
- # Update the corresponding property in my_network
11259
- setattr(my_network, network_properties[self.parent().active_channel], result)
12031
+ self.parent().load_channel(self.parent().active_channel, result, True)
11260
12032
 
11261
12033
  self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11262
12034
  self.accept()
@@ -11279,7 +12051,7 @@ class LabelDialog(QDialog):
11279
12051
  class SLabelDialog(QDialog):
11280
12052
  def __init__(self, parent=None):
11281
12053
  super().__init__(parent)
11282
- 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?")
11283
12055
  self.setModal(True)
11284
12056
 
11285
12057
  layout = QFormLayout(self)
@@ -11291,7 +12063,7 @@ class SLabelDialog(QDialog):
11291
12063
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
11292
12064
  layout.addRow("Prelabeled Array:", self.mode_selector)
11293
12065
 
11294
- layout.addRow(QLabel("Will Label Neighborhoods in: "))
12066
+ layout.addRow(QLabel("Will Label Binary Foreground Voxels in: "))
11295
12067
 
11296
12068
  # Add mode selection dropdown
11297
12069
  self.target_selector = QComboBox()
@@ -11303,10 +12075,20 @@ class SLabelDialog(QDialog):
11303
12075
  self.GPU = QPushButton("GPU")
11304
12076
  self.GPU.setCheckable(True)
11305
12077
  self.GPU.setChecked(False)
11306
- layout.addRow("Use GPU:", self.GPU)
12078
+ #layout.addRow("Use GPU:", self.GPU)
11307
12079
 
11308
12080
  self.down_factor = QLineEdit("")
11309
- 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)
11310
12092
 
11311
12093
  # Add Run button
11312
12094
  run_button = QPushButton("Run Smart Label")
@@ -11319,7 +12101,9 @@ class SLabelDialog(QDialog):
11319
12101
 
11320
12102
  accepted_source = self.mode_selector.currentIndex()
11321
12103
  accepted_target = self.target_selector.currentIndex()
12104
+ label_mode = self.label_mode.currentIndex()
11322
12105
  GPU = self.GPU.isChecked()
12106
+ fix = self.fix.isChecked()
11323
12107
 
11324
12108
 
11325
12109
  if accepted_source == accepted_target:
@@ -11332,27 +12116,51 @@ class SLabelDialog(QDialog):
11332
12116
  down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
11333
12117
 
11334
12118
 
11335
- try:
11336
-
11337
- # Update both the display data and the network object
11338
- binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
12119
+ if label_mode == 1:
11339
12120
 
11340
- label_array = sdl.invert_array(label_array)
12121
+ label_mask = label_array == 0
11341
12122
 
11342
- binary_array = binary_array * label_array
11343
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
11344
12137
  self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11345
-
11346
12138
  self.accept()
11347
-
11348
- except Exception as e:
11349
- QMessageBox.critical(
11350
- self,
11351
- "Error",
11352
- f"Error running smart label: {str(e)}"
11353
- )
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
+ )
11354
12159
 
11355
12160
  except Exception as e:
12161
+ import traceback
12162
+ traceback.print_exc()
12163
+
11356
12164
  QMessageBox.critical(
11357
12165
  self,
11358
12166
  "Error",
@@ -11364,7 +12172,7 @@ class ThresholdDialog(QDialog):
11364
12172
  def __init__(self, parent=None):
11365
12173
  super().__init__(parent)
11366
12174
  self.setWindowTitle("Choose Threshold Mode")
11367
- self.setModal(True)
12175
+ self.setModal(False)
11368
12176
 
11369
12177
  layout = QFormLayout(self)
11370
12178
 
@@ -11553,33 +12361,40 @@ class ExcelotronManager(QObject):
11553
12361
 
11554
12362
  class MachineWindow(QMainWindow):
11555
12363
 
11556
- def __init__(self, parent=None, GPU = False):
12364
+ def __init__(self, parent=None, GPU = False, tutorial_example = False):
11557
12365
  super().__init__(parent)
11558
12366
 
11559
12367
  try:
11560
12368
 
11561
- if self.parent().active_channel == 0:
11562
- if self.parent().channel_data[0] is not None:
11563
- try:
11564
- active_data = self.parent().channel_data[0]
11565
- act_channel = 0
11566
- 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:
11567
12381
  active_data = self.parent().channel_data[1]
11568
12382
  act_channel = 1
11569
- else:
11570
- active_data = self.parent().channel_data[1]
11571
- act_channel = 1
11572
12383
 
11573
- try:
11574
- if len(active_data.shape) == 3:
11575
- array1 = np.zeros_like(active_data).astype(np.uint8)
11576
- elif len(active_data.shape) == 4:
11577
- array1 = np.zeros_like(active_data)[:,:,:,0].astype(np.uint8)
11578
- except:
11579
- print("No data in nodes channel")
11580
- 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)")
11581
12397
 
11582
- self.setWindowTitle("Threshold")
11583
12398
 
11584
12399
  # Create central widget and layout
11585
12400
  central_widget = QWidget()
@@ -11600,22 +12415,23 @@ class MachineWindow(QMainWindow):
11600
12415
 
11601
12416
  self.parent().pen_button.setEnabled(False)
11602
12417
 
11603
- array3 = np.zeros_like(array1).astype(np.uint8)
11604
- 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
11605
12421
 
11606
- self.parent().load_channel(2, array1, True)
11607
- # Enable the channel button
11608
- # Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
11609
- if not self.parent().channel_buttons[2].isEnabled():
11610
- self.parent().channel_buttons[2].setEnabled(True)
11611
- self.parent().channel_buttons[2].click()
11612
- 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)
11613
12429
 
11614
- if len(active_data.shape) == 3:
11615
- self.parent().base_colors[act_channel] = self.parent().color_dictionary['WHITE']
11616
- 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']
11617
12433
 
11618
- self.parent().update_display()
12434
+ self.parent().update_display()
11619
12435
 
11620
12436
  # Set a reasonable default size for the window
11621
12437
  self.setMinimumWidth(600) # Increased to accommodate grouped buttons
@@ -11821,8 +12637,7 @@ class MachineWindow(QMainWindow):
11821
12637
  self.num_chunks = 0
11822
12638
  self.parent().update_display()
11823
12639
  except:
11824
- import traceback
11825
- traceback.print_exc()
12640
+
11826
12641
  pass
11827
12642
 
11828
12643
  except:
@@ -12221,34 +13036,43 @@ class MachineWindow(QMainWindow):
12221
13036
 
12222
13037
  def closeEvent(self, event):
12223
13038
  try:
12224
- if self.parent() and self.parent().isVisible():
12225
- if self.confirm_close_dialog():
12226
- # Clean up resources before closing
12227
- if self.brush_button.isChecked():
12228
- self.silence_button()
12229
- self.toggle_brush_mode()
12230
-
12231
- self.parent().pen_button.setEnabled(True)
12232
- self.parent().brush_mode = False
12233
-
12234
- # Kill the segmentation thread and wait for it to finish
12235
- self.kill_segmentation()
12236
- time.sleep(0.2) # Give additional time for cleanup
12237
- try:
12238
- self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
12239
- self.update_display()
12240
- except:
12241
- pass
12242
-
12243
- self.parent().machine_window = None
12244
- 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
12245
13063
  else:
12246
- 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()
12247
13068
  else:
12248
- # Parent doesn't exist or isn't visible, just close
12249
- if hasattr(self, 'parent') and self.parent():
12250
- self.parent().machine_window = None
12251
- 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
+
12252
13076
  except Exception as e:
12253
13077
  print(f"Error in closeEvent: {e}")
12254
13078
  # Even if there's an error, allow the window to close
@@ -12356,6 +13180,7 @@ class ThresholdWindow(QMainWindow):
12356
13180
 
12357
13181
  def __init__(self, parent=None, accepted_mode=0):
12358
13182
  super().__init__(parent)
13183
+ self.parent().thresh_window_ref = self
12359
13184
  self.setWindowTitle("Threshold")
12360
13185
 
12361
13186
  self.accepted_mode = accepted_mode
@@ -12400,19 +13225,28 @@ class ThresholdWindow(QMainWindow):
12400
13225
  data = self.parent().channel_data[self.parent().active_channel]
12401
13226
  nonzero_data = data[data != 0]
12402
13227
 
12403
- if nonzero_data.size > 578009537:
12404
- # For large arrays, use numpy histogram directly
12405
- counts, bin_edges = np.histogram(nonzero_data, bins= min(int(np.sqrt(nonzero_data.size)), 500), density=False)
12406
- # Store min/max separately if needed elsewhere
12407
- self.data_min = np.min(nonzero_data)
12408
- self.data_max = np.max(nonzero_data)
12409
- 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)
12410
13241
  else:
12411
- # For smaller arrays, can still use histogram method for consistency
12412
- counts, bin_edges = np.histogram(nonzero_data, bins='auto', density=False)
12413
- self.data_min = np.min(nonzero_data)
12414
- self.data_max = np.max(nonzero_data)
12415
- 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)
12416
13250
 
12417
13251
  self.bounds = True
12418
13252
  self.parent().bounds = True
@@ -12509,10 +13343,8 @@ class ThresholdWindow(QMainWindow):
12509
13343
  self.processing_cancelled.emit()
12510
13344
  self.close()
12511
13345
 
12512
- def closeEvent(self, event):
12513
- self.parent().preview = False
12514
- self.parent().targs = None
12515
- self.parent().bounds = False
13346
+ def make_full_highlight(self):
13347
+
12516
13348
  try: # could probably be refactored but this just handles keeping the highlight elements if the user presses X
12517
13349
  if self.chan == 0:
12518
13350
  if not self.bounds:
@@ -12546,6 +13378,14 @@ class ThresholdWindow(QMainWindow):
12546
13378
  pass
12547
13379
 
12548
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
+
12549
13389
  def get_values_in_range_all_vols(self, chan, min_val, max_val):
12550
13390
  output = []
12551
13391
  if self.accepted_mode == 1:
@@ -12812,36 +13652,33 @@ class SmartDilateDialog(QDialog):
12812
13652
 
12813
13653
 
12814
13654
  class DilateDialog(QDialog):
12815
- def __init__(self, parent=None):
13655
+ def __init__(self, parent=None, args = None):
12816
13656
  super().__init__(parent)
12817
13657
  self.setWindowTitle("Dilate Parameters")
12818
- self.setModal(True)
13658
+ self.setModal(False)
12819
13659
 
12820
13660
  layout = QFormLayout(self)
12821
13661
 
12822
- self.amount = QLineEdit("1")
12823
- layout.addRow("Dilation Radius:", self.amount)
12824
-
12825
- if my_network.xy_scale is not None:
12826
- xy_scale = f"{my_network.xy_scale}"
13662
+ if args:
13663
+ self.parent().last_dil = args[0]
13664
+ self.index = 1
12827
13665
  else:
12828
- xy_scale = "1"
13666
+ self.parent().last_dil = 1
13667
+ self.index = 0
12829
13668
 
12830
- self.xy_scale = QLineEdit(xy_scale)
12831
- layout.addRow("xy_scale:", self.xy_scale)
13669
+ self.amount = QLineEdit(f"{self.parent().last_dil}")
13670
+ layout.addRow("Dilation Radius:", self.amount)
12832
13671
 
12833
- if my_network.z_scale is not None:
12834
- z_scale = f"{my_network.z_scale}"
12835
- else:
12836
- z_scale = "1"
13672
+ self.xy_scale = QLineEdit("1")
13673
+ layout.addRow("xy_scale:", self.xy_scale)
12837
13674
 
12838
- self.z_scale = QLineEdit(z_scale)
13675
+ self.z_scale = QLineEdit("1")
12839
13676
  layout.addRow("z_scale:", self.z_scale)
12840
13677
 
12841
13678
  # Add mode selection dropdown
12842
13679
  self.mode_selector = QComboBox()
12843
- self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small dilations)", "Preserve Labels (slower)", "Distance Transform-Based (Slower but more accurate at larger dilations)"])
12844
- 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
12845
13682
  layout.addRow("Execution Mode:", self.mode_selector)
12846
13683
 
12847
13684
  # Add Run button
@@ -12883,13 +13720,15 @@ class DilateDialog(QDialog):
12883
13720
  if active_data is None:
12884
13721
  raise ValueError("No active image selected")
12885
13722
 
13723
+ self.parent().last_dil = amount
13724
+
12886
13725
  if accepted_mode == 1:
12887
13726
  dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
12888
13727
  dialog.exec()
12889
13728
  self.accept()
12890
13729
  return
12891
13730
 
12892
- if accepted_mode == 2:
13731
+ if accepted_mode == 0:
12893
13732
  result = n3d.dilate_3D_dt(active_data, amount, xy_scaling = xy_scale, z_scaling = z_scale)
12894
13733
  else:
12895
13734
 
@@ -12919,36 +13758,33 @@ class DilateDialog(QDialog):
12919
13758
  )
12920
13759
 
12921
13760
  class ErodeDialog(QDialog):
12922
- def __init__(self, parent=None):
13761
+ def __init__(self, parent=None, args = None):
12923
13762
  super().__init__(parent)
12924
13763
  self.setWindowTitle("Erosion Parameters")
12925
13764
  self.setModal(True)
12926
13765
 
12927
13766
  layout = QFormLayout(self)
12928
13767
 
12929
- self.amount = QLineEdit("1")
12930
- layout.addRow("Erosion Radius:", self.amount)
12931
-
12932
- if my_network.xy_scale is not None:
12933
- xy_scale = f"{my_network.xy_scale}"
13768
+ if args:
13769
+ self.parent().last_ero = args[0]
13770
+ self.index = 1
12934
13771
  else:
12935
- xy_scale = "1"
13772
+ self.parent().last_ero = 1
13773
+ self.index = 0
12936
13774
 
12937
- self.xy_scale = QLineEdit(xy_scale)
12938
- layout.addRow("xy_scale:", self.xy_scale)
13775
+ self.amount = QLineEdit(f"{self.parent().last_ero}")
13776
+ layout.addRow("Erosion Radius:", self.amount)
12939
13777
 
12940
- if my_network.z_scale is not None:
12941
- z_scale = f"{my_network.z_scale}"
12942
- else:
12943
- z_scale = "1"
13778
+ self.xy_scale = QLineEdit("1")
13779
+ layout.addRow("xy_scale:", self.xy_scale)
12944
13780
 
12945
- self.z_scale = QLineEdit(z_scale)
13781
+ self.z_scale = QLineEdit("1")
12946
13782
  layout.addRow("z_scale:", self.z_scale)
12947
13783
 
12948
13784
  # Add mode selection dropdown
12949
13785
  self.mode_selector = QComboBox()
12950
- self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small erosions)", "Distance Transform-Based (Slower but more accurate at larger dilations)", "Preserve Labels (Slower)"])
12951
- 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
12952
13788
  layout.addRow("Execution Mode:", self.mode_selector)
12953
13789
 
12954
13790
  # Add Run button
@@ -12984,8 +13820,7 @@ class ErodeDialog(QDialog):
12984
13820
 
12985
13821
  mode = self.mode_selector.currentIndex()
12986
13822
 
12987
- if mode == 2:
12988
- mode = 1
13823
+ if mode == 1:
12989
13824
  preserve_labels = True
12990
13825
  else:
12991
13826
  preserve_labels = False
@@ -13007,7 +13842,7 @@ class ErodeDialog(QDialog):
13007
13842
 
13008
13843
 
13009
13844
  self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
13010
-
13845
+ self.parent().last_ero = amount
13011
13846
  self.accept()
13012
13847
 
13013
13848
  except Exception as e:
@@ -13037,6 +13872,11 @@ class HoleDialog(QDialog):
13037
13872
  self.borders.setChecked(False)
13038
13873
  layout.addRow("Fill Small Holes Along Borders:", self.borders)
13039
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
+
13040
13880
  self.sep_holes = QPushButton("Seperate Hole Mask")
13041
13881
  self.sep_holes.setCheckable(True)
13042
13882
  self.sep_holes.setChecked(False)
@@ -13059,6 +13899,9 @@ class HoleDialog(QDialog):
13059
13899
  borders = self.borders.isChecked()
13060
13900
  headon = self.headon.isChecked()
13061
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)
13062
13905
 
13063
13906
  if borders:
13064
13907
 
@@ -13077,7 +13920,11 @@ class HoleDialog(QDialog):
13077
13920
  fill_borders = borders
13078
13921
  )
13079
13922
 
13923
+
13080
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
+
13081
13928
  self.parent().load_channel(self.parent().active_channel, result, True)
13082
13929
  else:
13083
13930
  self.parent().load_channel(3, active_data - result, True)
@@ -13093,6 +13940,135 @@ class HoleDialog(QDialog):
13093
13940
  f"Error running fill holes: {str(e)}"
13094
13941
  )
13095
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
+
13096
14072
  class MaskDialog(QDialog):
13097
14073
 
13098
14074
  def __init__(self, parent=None):
@@ -13422,7 +14398,13 @@ class SkeletonizeDialog(QDialog):
13422
14398
  # auto checkbox (default True)
13423
14399
  self.auto = QPushButton("Auto")
13424
14400
  self.auto.setCheckable(True)
13425
- 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)
13426
14408
  layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
13427
14409
 
13428
14410
  # Add Run button
@@ -13478,6 +14460,86 @@ class SkeletonizeDialog(QDialog):
13478
14460
  f"Error running skeletonize: {str(e)}"
13479
14461
  )
13480
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
+
13481
14543
  class DistanceDialog(QDialog):
13482
14544
  def __init__(self, parent=None):
13483
14545
  super().__init__(parent)
@@ -13596,11 +14658,6 @@ class WatershedDialog(QDialog):
13596
14658
  self.setModal(True)
13597
14659
 
13598
14660
  layout = QFormLayout(self)
13599
-
13600
- # Directory (empty by default)
13601
- self.directory = QLineEdit()
13602
- self.directory.setPlaceholderText("Leave empty for None")
13603
- layout.addRow("Output Directory:", self.directory)
13604
14661
 
13605
14662
  try:
13606
14663
 
@@ -13651,7 +14708,7 @@ class WatershedDialog(QDialog):
13651
14708
  def run_watershed(self):
13652
14709
  try:
13653
14710
  # Get directory (None if empty)
13654
- directory = self.directory.text() if self.directory.text() else None
14711
+ directory = None
13655
14712
 
13656
14713
  # Get proportion (0.1 if empty or invalid)
13657
14714
  try:
@@ -13895,7 +14952,7 @@ class GenNodesDialog(QDialog):
13895
14952
  def __init__(self, parent=None, down_factor=None, called=False):
13896
14953
  super().__init__(parent)
13897
14954
  self.setWindowTitle("Create Nodes from Edge Vertices")
13898
- self.setModal(True)
14955
+ self.setModal(False)
13899
14956
 
13900
14957
  # Main layout
13901
14958
  main_layout = QVBoxLayout(self)
@@ -13919,15 +14976,15 @@ class GenNodesDialog(QDialog):
13919
14976
  self.cubic = QPushButton("Cubic Downsample")
13920
14977
  self.cubic.setCheckable(True)
13921
14978
  self.cubic.setChecked(False)
13922
- process_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
13923
- 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)
13924
14981
 
13925
14982
  # Fast dilation checkbox
13926
14983
  self.fast_dil = QPushButton("Fast-Dil")
13927
14984
  self.fast_dil.setCheckable(True)
13928
14985
  self.fast_dil.setChecked(True)
13929
- process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 2, 0)
13930
- 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)
13931
14988
 
13932
14989
  process_group.setLayout(process_layout)
13933
14990
  main_layout.addWidget(process_group)
@@ -13936,17 +14993,17 @@ class GenNodesDialog(QDialog):
13936
14993
  self.cubic = down_factor[1]
13937
14994
 
13938
14995
  # Fast dilation checkbox (still needed even if down_factor is provided)
13939
- process_group = QGroupBox("Processing Options")
13940
- process_layout = QGridLayout()
14996
+ #process_group = QGroupBox("Processing Options")
14997
+ #process_layout = QGridLayout()
13941
14998
 
13942
14999
  self.fast_dil = QPushButton("Fast-Dil")
13943
15000
  self.fast_dil.setCheckable(True)
13944
15001
  self.fast_dil.setChecked(True)
13945
- process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 0, 0)
13946
- 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)
13947
15004
 
13948
- process_group.setLayout(process_layout)
13949
- main_layout.addWidget(process_group)
15005
+ #process_group.setLayout(process_layout)
15006
+ #main_layout.addWidget(process_group)
13950
15007
 
13951
15008
  # --- Recommended Corrections Group ---
13952
15009
  rec_group = QGroupBox("Recommended Corrections")
@@ -13979,8 +15036,8 @@ class GenNodesDialog(QDialog):
13979
15036
 
13980
15037
  # Max volume
13981
15038
  self.max_vol = QLineEdit("0")
13982
- opt_layout.addWidget(QLabel("Maximum Voxel Volume to Retain (Compensates for skeleton looping):"), 0, 0)
13983
- 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)
13984
15041
 
13985
15042
  # Component dilation
13986
15043
  self.comp_dil = QLineEdit("0")
@@ -14056,6 +15113,8 @@ class GenNodesDialog(QDialog):
14056
15113
 
14057
15114
  fastdil = self.fast_dil.isChecked()
14058
15115
 
15116
+ if down_factor > 1:
15117
+ my_network.edges = n3d.downsample(my_network.edges, down_factor)
14059
15118
 
14060
15119
  if auto:
14061
15120
  my_network.edges = n3d.skeletonize(my_network.edges)
@@ -14067,11 +15126,9 @@ class GenNodesDialog(QDialog):
14067
15126
  max_vol=max_vol,
14068
15127
  branch_removal=branch_removal,
14069
15128
  comp_dil=comp_dil,
14070
- down_factor=down_factor,
14071
15129
  order = order,
14072
15130
  return_skele = True,
14073
15131
  fastdil = fastdil
14074
-
14075
15132
  )
14076
15133
 
14077
15134
  if down_factor > 0 and not self.called:
@@ -14122,10 +15179,10 @@ class GenNodesDialog(QDialog):
14122
15179
 
14123
15180
  class BranchDialog(QDialog):
14124
15181
 
14125
- def __init__(self, parent=None, called = False):
15182
+ def __init__(self, parent=None, called = False, tutorial_example = False):
14126
15183
  super().__init__(parent)
14127
15184
  self.setWindowTitle("Label Branches (of edges)")
14128
- self.setModal(True)
15185
+ self.setModal(False)
14129
15186
 
14130
15187
  # Main layout
14131
15188
  main_layout = QVBoxLayout(self)
@@ -14138,33 +15195,40 @@ class BranchDialog(QDialog):
14138
15195
  self.fix = QPushButton("Auto-Correct 1")
14139
15196
  self.fix.setCheckable(True)
14140
15197
  self.fix.setChecked(False)
14141
- correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
14142
- 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)
14143
15200
 
14144
15201
  # Fix value
14145
15202
  self.fix_val = QLineEdit('4')
14146
- correction_layout.addWidget(QLabel("Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
14147
- 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)
14148
15205
 
14149
15206
  # Seed
14150
15207
  self.seed = QLineEdit('')
14151
- correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
14152
- 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)
14153
15210
 
14154
- self.fix2 = QPushButton("Auto-Correct 2")
15211
+ self.fix2 = QPushButton("Auto-Correct Internal Branches")
14155
15212
  self.fix2.setCheckable(True)
14156
15213
  self.fix2.setChecked(True)
14157
15214
  correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Internal Labels: "), 3, 0)
14158
15215
  correction_layout.addWidget(self.fix2, 3, 1)
14159
15216
 
14160
- self.fix3 = QPushButton("Split Nontouching Branches?")
15217
+ self.fix3 = QPushButton("Auto-Correct Nontouching Branches")
14161
15218
  self.fix3.setCheckable(True)
14162
- if called:
14163
- self.fix3.setChecked(True)
14164
- else:
14165
- self.fix3.setChecked(False)
14166
- 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)
14167
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)
14168
15232
 
14169
15233
  correction_group.setLayout(correction_layout)
14170
15234
  main_layout.addWidget(correction_group)
@@ -14182,8 +15246,8 @@ class BranchDialog(QDialog):
14182
15246
  self.cubic = QPushButton("Cubic Downsample")
14183
15247
  self.cubic.setCheckable(True)
14184
15248
  self.cubic.setChecked(False)
14185
- processing_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
14186
- 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)
14187
15251
 
14188
15252
  processing_group.setLayout(processing_layout)
14189
15253
  main_layout.addWidget(processing_group)
@@ -14191,20 +15255,27 @@ class BranchDialog(QDialog):
14191
15255
  # --- Misc Options Group ---
14192
15256
  misc_group = QGroupBox("Misc Options")
14193
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)
14194
15265
 
14195
15266
  # Nodes checkbox
14196
15267
  self.nodes = QPushButton("Generate Nodes")
14197
15268
  self.nodes.setCheckable(True)
14198
15269
  self.nodes.setChecked(True)
14199
- misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"), 0, 0)
14200
- 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)
14201
15272
 
14202
15273
  # GPU checkbox
14203
15274
  self.GPU = QPushButton("GPU")
14204
15275
  self.GPU.setCheckable(True)
14205
15276
  self.GPU.setChecked(False)
14206
- misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"), 1, 0)
14207
- 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)
14208
15279
 
14209
15280
  misc_group.setLayout(misc_layout)
14210
15281
  main_layout.addWidget(misc_group)
@@ -14214,7 +15285,7 @@ class BranchDialog(QDialog):
14214
15285
  run_button.clicked.connect(self.branch_label)
14215
15286
  main_layout.addWidget(run_button)
14216
15287
 
14217
- 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:
14218
15289
  QMessageBox.critical(
14219
15290
  self,
14220
15291
  "Alert",
@@ -14236,8 +15307,11 @@ class BranchDialog(QDialog):
14236
15307
  fix = self.fix.isChecked()
14237
15308
  fix2 = self.fix2.isChecked()
14238
15309
  fix3 = self.fix3.isChecked()
15310
+ fix4 = self.fix4.isChecked()
14239
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
14240
15313
  seed = int(self.seed.text()) if self.seed.text() else None
15314
+ compute = self.compute.isChecked()
14241
15315
 
14242
15316
  if my_network.edges is None and my_network.nodes is not None:
14243
15317
  self.parent().load_channel(1, my_network.nodes, data = True)
@@ -14254,7 +15328,12 @@ class BranchDialog(QDialog):
14254
15328
 
14255
15329
  if my_network.edges is not None and my_network.nodes is not None and my_network.id_overlay is not None:
14256
15330
 
14257
- 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)
14258
15337
 
14259
15338
  if fix2:
14260
15339
 
@@ -14291,7 +15370,23 @@ class BranchDialog(QDialog):
14291
15370
 
14292
15371
  if fix3:
14293
15372
 
14294
- 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')
14295
15390
 
14296
15391
 
14297
15392
  if down_factor is not None:
@@ -14467,7 +15562,7 @@ class ModifyDialog(QDialog):
14467
15562
  def __init__(self, parent=None):
14468
15563
  super().__init__(parent)
14469
15564
  self.setWindowTitle("Modify Network Qualities")
14470
- self.setModal(True)
15565
+ self.setModal(False)
14471
15566
  layout = QFormLayout(self)
14472
15567
 
14473
15568
  self.revid = QPushButton("Remove Unassigned")
@@ -14692,10 +15787,6 @@ class CentroidDialog(QDialog):
14692
15787
 
14693
15788
  layout = QFormLayout(self)
14694
15789
 
14695
- self.directory = QLineEdit()
14696
- self.directory.setPlaceholderText("Leave empty for active directory")
14697
- layout.addRow("Output Directory:", self.directory)
14698
-
14699
15790
  self.downsample = QLineEdit("1")
14700
15791
  layout.addRow("Downsample Factor:", self.downsample)
14701
15792
 
@@ -14725,7 +15816,7 @@ class CentroidDialog(QDialog):
14725
15816
  ignore_empty = self.ignore_empty.isChecked()
14726
15817
 
14727
15818
  # Get directory (None if empty)
14728
- directory = self.directory.text() if self.directory.text() else None
15819
+ directory = None
14729
15820
 
14730
15821
  # Get downsample
14731
15822
  try:
@@ -14806,7 +15897,6 @@ class CentroidDialog(QDialog):
14806
15897
 
14807
15898
  class CalcAllDialog(QDialog):
14808
15899
  # Class variables to store previous settings
14809
- prev_directory = ""
14810
15900
  prev_search = ""
14811
15901
  prev_diledge = ""
14812
15902
  prev_down_factor = ""
@@ -14823,7 +15913,7 @@ class CalcAllDialog(QDialog):
14823
15913
  def __init__(self, parent=None):
14824
15914
  super().__init__(parent)
14825
15915
  self.setWindowTitle("Calculate Connectivity Network Parameters")
14826
- self.setModal(True)
15916
+ self.setModal(False)
14827
15917
 
14828
15918
  # Main layout
14829
15919
  main_layout = QVBoxLayout(self)
@@ -14859,7 +15949,7 @@ class CalcAllDialog(QDialog):
14859
15949
 
14860
15950
  self.other_nodes = QLineEdit(self.prev_other_nodes)
14861
15951
  self.other_nodes.setPlaceholderText("Leave empty for None")
14862
- 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)
14863
15953
 
14864
15954
  self.remove_trunk = QLineEdit(self.prev_remove_trunk)
14865
15955
  self.remove_trunk.setPlaceholderText("Leave empty for 0")
@@ -14882,17 +15972,17 @@ class CalcAllDialog(QDialog):
14882
15972
 
14883
15973
  self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
14884
15974
  self.GPU_downsample.setPlaceholderText("Leave empty for None")
14885
- 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)
14886
15976
 
14887
15977
  self.gpu = QPushButton("GPU")
14888
15978
  self.gpu.setCheckable(True)
14889
15979
  self.gpu.setChecked(self.prev_gpu)
14890
- speedup_layout.addRow("Use GPU:", self.gpu)
15980
+ #speedup_layout.addRow("Use GPU:", self.gpu)
14891
15981
 
14892
15982
  self.fastdil = QPushButton("Fast Dilate")
14893
15983
  self.fastdil.setCheckable(True)
14894
15984
  self.fastdil.setChecked(self.prev_fastdil)
14895
- 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)
14896
15986
 
14897
15987
  main_layout.addWidget(speedup_group)
14898
15988
 
@@ -14900,10 +15990,6 @@ class CalcAllDialog(QDialog):
14900
15990
  output_group = QGroupBox("Output Options")
14901
15991
  output_layout = QFormLayout(output_group)
14902
15992
 
14903
- self.directory = QLineEdit(self.prev_directory)
14904
- self.directory.setPlaceholderText("Will Have to Save Manually If Empty")
14905
- output_layout.addRow("Output Directory:", self.directory)
14906
-
14907
15993
  self.overlays = QPushButton("Overlays")
14908
15994
  self.overlays.setCheckable(True)
14909
15995
  self.overlays.setChecked(self.prev_overlays)
@@ -14925,7 +16011,7 @@ class CalcAllDialog(QDialog):
14925
16011
 
14926
16012
  try:
14927
16013
  # Get directory (None if empty)
14928
- directory = self.directory.text() if self.directory.text() else None
16014
+ directory = None
14929
16015
 
14930
16016
  # Get xy_scale and z_scale (1 if empty or invalid)
14931
16017
  try:
@@ -15002,7 +16088,6 @@ class CalcAllDialog(QDialog):
15002
16088
  )
15003
16089
 
15004
16090
  # Store current values as previous values
15005
- CalcAllDialog.prev_directory = self.directory.text()
15006
16091
  CalcAllDialog.prev_search = self.search.text()
15007
16092
  CalcAllDialog.prev_diledge = self.diledge.text()
15008
16093
  CalcAllDialog.prev_down_factor = self.down_factor.text()
@@ -15106,10 +16191,10 @@ class CalcAllDialog(QDialog):
15106
16191
 
15107
16192
 
15108
16193
  class ProxDialog(QDialog):
15109
- def __init__(self, parent=None):
16194
+ def __init__(self, parent=None, tutorial_example = False):
15110
16195
  super().__init__(parent)
15111
16196
  self.setWindowTitle("Calculate Proximity Network")
15112
- self.setModal(True)
16197
+ self.setModal(False)
15113
16198
 
15114
16199
  # Main layout
15115
16200
  main_layout = QVBoxLayout(self)
@@ -15145,6 +16230,11 @@ class ProxDialog(QDialog):
15145
16230
  self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
15146
16231
  self.id_selector.setCurrentIndex(0) # Default to Mode 1
15147
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)
15148
16238
  else:
15149
16239
  self.id_selector = None
15150
16240
 
@@ -15154,10 +16244,6 @@ class ProxDialog(QDialog):
15154
16244
  output_group = QGroupBox("Output Options")
15155
16245
  output_layout = QFormLayout(output_group)
15156
16246
 
15157
- self.directory = QLineEdit('')
15158
- self.directory.setPlaceholderText("Leave empty for 'my_network'")
15159
- output_layout.addRow("Output Directory:", self.directory)
15160
-
15161
16247
  self.overlays = QPushButton("Overlays")
15162
16248
  self.overlays.setCheckable(True)
15163
16249
  self.overlays.setChecked(True)
@@ -15183,7 +16269,7 @@ class ProxDialog(QDialog):
15183
16269
  self.fastdil = QPushButton("Fast Dilate")
15184
16270
  self.fastdil.setCheckable(True)
15185
16271
  self.fastdil.setChecked(False)
15186
- 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)
15187
16273
 
15188
16274
  main_layout.addWidget(speedup_group)
15189
16275
 
@@ -15209,10 +16295,8 @@ class ProxDialog(QDialog):
15209
16295
  else:
15210
16296
  targets = None
15211
16297
 
15212
- try:
15213
- directory = self.directory.text() if self.directory.text() else None
15214
- except:
15215
- directory = None
16298
+ directory = None
16299
+
15216
16300
 
15217
16301
  # Get xy_scale and z_scale (1 if empty or invalid)
15218
16302
  try:
@@ -15962,7 +17046,186 @@ class HistogramSelector(QWidget):
15962
17046
  except Exception as e:
15963
17047
  print(f"Error generating dispersion histogram: {e}")
15964
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()
15965
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()
15966
17229
 
15967
17230
  # Initiating this program from the script line:
15968
17231