nettracer3d 0.8.0__py3-none-any.whl → 0.8.2__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.
@@ -43,11 +43,15 @@ class ImageViewerWindow(QMainWindow):
43
43
  self.setGeometry(100, 100, 1400, 800)
44
44
 
45
45
  # Initialize channel data and states
46
- self.channel_data = [None] * 4
47
- self.channel_visible = [False] * 4
46
+ self.channel_data = [None] * 5
47
+ self.channel_visible = [False] * 5
48
48
  self.current_slice = 0
49
49
  self.active_channel = 0 # Initialize active channel
50
50
  self.node_name = "Root_Nodes"
51
+ self.last_saved = None
52
+ self.last_saved_name = None
53
+ self.last_load = None
54
+ self.temp_chan = 0
51
55
 
52
56
  self.color_dictionary = {
53
57
  # Reds
@@ -129,7 +133,6 @@ class ImageViewerWindow(QMainWindow):
129
133
  self.img_width = None
130
134
  self.img_height = None
131
135
  self.pre_pan_channel_state = None # Store which channels were visible before pan
132
- self.pan_background = None # Store the static background image during pan
133
136
  self.is_pan_preview = False # Track if we're in pan preview mode
134
137
  self.pre_pan_channel_state = None # Store which channels were visible before pan
135
138
  self.pan_background_image = None # Store the rendered composite image
@@ -152,7 +155,7 @@ class ImageViewerWindow(QMainWindow):
152
155
  self.channel_brightness = [{
153
156
  'min': 0,
154
157
  'max': 1
155
- } for _ in range(4)]
158
+ } for _ in range(5)]
156
159
 
157
160
  # Create the brightness dialog but don't show it yet
158
161
  self.brightness_dialog = BrightnessContrastDialog(self)
@@ -445,6 +448,7 @@ class ImageViewerWindow(QMainWindow):
445
448
  self.paint_timer.timeout.connect(self.flush_paint_updates)
446
449
  self.paint_timer.setSingleShot(True)
447
450
  self.pending_paint_update = False
451
+ self.static_background = None
448
452
 
449
453
  # Threading for paint operations
450
454
  self.paint_queue = queue.Queue()
@@ -453,7 +457,6 @@ class ImageViewerWindow(QMainWindow):
453
457
  self.paint_worker.start()
454
458
 
455
459
  # Background caching for blitting
456
- self.paint_background = None
457
460
  self.paint_session_active = False
458
461
 
459
462
  # Batch paint operations
@@ -1503,6 +1506,11 @@ class ImageViewerWindow(QMainWindow):
1503
1506
  self.targ = QLineEdit("")
1504
1507
  layout.addRow("Node/Edge ID:", self.targ)
1505
1508
 
1509
+ self.mode_selector = QComboBox()
1510
+ self.mode_selector.addItems(["nodes", "edges", "communities"])
1511
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
1512
+ layout.addRow("Type to select:", self.mode_selector)
1513
+
1506
1514
  run_button = QPushButton("Enter")
1507
1515
  run_button.clicked.connect(self.run)
1508
1516
  layout.addWidget(run_button)
@@ -1511,20 +1519,22 @@ class ImageViewerWindow(QMainWindow):
1511
1519
 
1512
1520
  try:
1513
1521
 
1522
+ mode = self.mode_selector.currentIndex()
1523
+
1514
1524
  value = int(self.targ.text()) if self.targ.text().strip() else None
1515
1525
 
1516
1526
  if value is None:
1517
1527
  return
1518
1528
 
1519
- if self.parent().active_channel == 1:
1520
-
1521
- mode = 'edges'
1529
+ if mode == 1:
1522
1530
 
1523
1531
  if my_network.edge_centroids is None:
1524
1532
  self.parent().show_centroid_dialog()
1525
1533
 
1526
1534
  num = (self.parent().channel_data[1].shape[0] * self.parent().channel_data[1].shape[1] * self.parent().channel_data[1].shape[2])
1527
1535
 
1536
+ self.parent().clicked_values['edges'] = [value]
1537
+
1528
1538
  if value in my_network.edge_centroids:
1529
1539
 
1530
1540
  # Get centroid coordinates (Z, Y, X)
@@ -1545,30 +1555,42 @@ class ImageViewerWindow(QMainWindow):
1545
1555
 
1546
1556
  else:
1547
1557
 
1548
- mode = 'nodes'
1549
1558
 
1550
1559
  if my_network.node_centroids is None:
1551
1560
  self.parent().show_centroid_dialog()
1552
1561
 
1553
1562
  num = (self.parent().channel_data[0].shape[0] * self.parent().channel_data[0].shape[1] * self.parent().channel_data[0].shape[2])
1554
1563
 
1564
+ if mode == 0:
1565
+ self.parent().clicked_values['nodes'] = [value]
1566
+ elif mode == 2:
1567
+
1568
+ coms = n3d.invert_dict(my_network.communities)
1569
+ self.parent().clicked_values['nodes'] = coms[value]
1570
+ com = value
1571
+ value = coms[value][0]
1572
+
1573
+
1555
1574
  if value in my_network.node_centroids:
1556
1575
  # Get centroid coordinates (Z, Y, X)
1557
1576
  centroid = my_network.node_centroids[value]
1558
1577
  # Set the active channel to nodes (0)
1559
- self.parent().set_active_channel(0)
1578
+ self.parent().set_active_channel(0)
1560
1579
  # Toggle on the nodes channel if it's not already visible
1561
1580
  if not self.parent().channel_visible[0]:
1562
1581
  self.parent().channel_buttons[0].setChecked(True)
1563
1582
  self.parent().toggle_channel(0)
1564
1583
  # Navigate to the Z-slice
1565
1584
  self.parent().slice_slider.setValue(int(centroid[0]))
1566
- print(f"Found node {value} at Z-slice {centroid[0]}")
1585
+ if mode == 0:
1586
+ print(f"Found node {value} at Z-slice {centroid[0]}")
1587
+ elif mode == 2:
1588
+ print(f"Found node {value} from community {com} at Z-slice {centroid[0]}")
1589
+
1567
1590
 
1568
1591
  else:
1569
1592
  print(f"Node {value} not found in centroids dictionary")
1570
1593
 
1571
- self.parent().clicked_values[mode] = [value]
1572
1594
 
1573
1595
  if num > self.parent().mini_thresh:
1574
1596
  self.parent().mini_overlay = True
@@ -2040,8 +2062,15 @@ class ImageViewerWindow(QMainWindow):
2040
2062
  def toggle_zoom_mode(self):
2041
2063
  """Toggle zoom mode on/off."""
2042
2064
  self.zoom_mode = self.zoom_button.isChecked()
2065
+
2043
2066
  if self.zoom_mode:
2044
2067
  self.pan_button.setChecked(False)
2068
+
2069
+ if self.pan_mode or self.brush_mode:
2070
+ current_xlim = self.ax.get_xlim()
2071
+ current_ylim = self.ax.get_ylim()
2072
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
2073
+
2045
2074
  self.pen_button.setChecked(False)
2046
2075
  self.pan_mode = False
2047
2076
  self.brush_mode = False
@@ -2062,6 +2091,11 @@ class ImageViewerWindow(QMainWindow):
2062
2091
  """Toggle pan mode on/off."""
2063
2092
  self.pan_mode = self.pan_button.isChecked()
2064
2093
  if self.pan_mode:
2094
+ if self.brush_mode:
2095
+ current_xlim = self.ax.get_xlim()
2096
+ current_ylim = self.ax.get_ylim()
2097
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
2098
+
2065
2099
  self.zoom_button.setChecked(False)
2066
2100
  self.pen_button.setChecked(False)
2067
2101
  self.zoom_mode = False
@@ -2073,6 +2107,9 @@ class ImageViewerWindow(QMainWindow):
2073
2107
  self.machine_window.silence_button()
2074
2108
  self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
2075
2109
  else:
2110
+ current_xlim = self.ax.get_xlim()
2111
+ current_ylim = self.ax.get_ylim()
2112
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
2076
2113
  if self.machine_window is None:
2077
2114
  self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
2078
2115
  else:
@@ -2082,12 +2119,23 @@ class ImageViewerWindow(QMainWindow):
2082
2119
  """Toggle brush mode on/off"""
2083
2120
  self.brush_mode = self.pen_button.isChecked()
2084
2121
  if self.brush_mode:
2122
+
2123
+ if self.pan_mode:
2124
+ current_xlim = self.ax.get_xlim()
2125
+ current_ylim = self.ax.get_ylim()
2126
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
2127
+
2085
2128
  self.pan_button.setChecked(False)
2086
2129
  self.zoom_button.setChecked(False)
2087
2130
  self.pan_mode = False
2088
2131
  self.zoom_mode = False
2089
2132
  self.update_brush_cursor()
2090
2133
  else:
2134
+ # Get current zoom and do display update
2135
+ current_xlim = self.ax.get_xlim()
2136
+ current_ylim = self.ax.get_ylim()
2137
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
2138
+
2091
2139
  self.last_change = None
2092
2140
  self.can = False
2093
2141
  self.threed = False
@@ -2208,13 +2256,29 @@ class ImageViewerWindow(QMainWindow):
2208
2256
  self.high_button.click()
2209
2257
  if event.key() == Qt.Key_F and event.modifiers() == Qt.ShiftModifier:
2210
2258
  self.handle_find()
2259
+ if event.key() == Qt.Key_S and event.modifiers() == Qt.ControlModifier:
2260
+ self.handle_resave()
2261
+ if event.key() == Qt.Key_L and event.modifiers() == Qt.ControlModifier:
2262
+ self.load_from_network_obj(directory = self.last_load)
2211
2263
  if self.brush_mode and self.machine_window is None:
2212
2264
  if event.key() == Qt.Key_F:
2213
2265
  self.toggle_can()
2214
2266
  elif event.key() == Qt.Key_D:
2215
2267
  self.toggle_threed()
2216
2268
 
2269
+ def handle_resave(self, asbool = True):
2270
+
2271
+ try:
2272
+
2273
+ if self.last_saved is None:
2217
2274
 
2275
+ self.save_network_3d()
2276
+
2277
+ else:
2278
+ my_network.dump(parent_dir=self.last_saved, name=self.last_save_name)
2279
+
2280
+ except Exception as e:
2281
+ print(f"Error saving: {e}")
2218
2282
 
2219
2283
  def update_brush_cursor(self):
2220
2284
  """Update the cursor to show brush size"""
@@ -2329,7 +2393,7 @@ class ImageViewerWindow(QMainWindow):
2329
2393
  return data_coords[0], data_coords[1]
2330
2394
 
2331
2395
  def on_mouse_press(self, event):
2332
- """Handle mouse press events - OPTIMIZED VERSION."""
2396
+ """Handle mouse press events."""
2333
2397
  if event.inaxes != self.ax:
2334
2398
  return
2335
2399
 
@@ -2337,70 +2401,32 @@ class ImageViewerWindow(QMainWindow):
2337
2401
  self.pan_button.click()
2338
2402
  return
2339
2403
 
2340
- if self.zoom_mode:
2341
- # Handle zoom mode press
2342
- if self.original_xlim is None:
2343
- self.original_xlim = self.ax.get_xlim()
2344
- self.original_ylim = self.ax.get_ylim()
2345
-
2346
- current_xlim = self.ax.get_xlim()
2347
- current_ylim = self.ax.get_ylim()
2348
- xdata = event.xdata
2349
- ydata = event.ydata
2350
-
2351
- if event.button == 1: # Left click - zoom in
2352
- x_range = (current_xlim[1] - current_xlim[0]) / 4
2353
- y_range = (current_ylim[1] - current_ylim[0]) / 4
2354
-
2355
- self.ax.set_xlim([xdata - x_range, xdata + x_range])
2356
- self.ax.set_ylim([ydata - y_range, ydata + y_range])
2404
+ if self.pan_mode:
2357
2405
 
2358
- self.zoom_changed = True # Flag that zoom has changed
2406
+ self.panning = True
2407
+ self.pan_start = (event.xdata, event.ydata)
2408
+
2409
+ if self.pan_background_image is None:
2410
+
2411
+ if self.machine_window is not None:
2412
+ if self.machine_window.segmentation_worker is not None:
2413
+ self.machine_window.segmentation_worker.pause()
2359
2414
 
2360
- if not hasattr(self, 'zoom_changed'):
2361
- self.zoom_changed = False
2415
+ # Store current channel visibility state
2416
+ self.pre_pan_channel_state = self.channel_visible.copy()
2362
2417
 
2363
- elif event.button == 3: # Right click - zoom out
2364
- x_range = (current_xlim[1] - current_xlim[0])
2365
- y_range = (current_ylim[1] - current_ylim[0])
2418
+ # Create static background from currently visible channels
2419
+ self.create_pan_background()
2366
2420
 
2367
- new_xlim = [xdata - x_range, xdata + x_range]
2368
- new_ylim = [ydata - y_range, ydata + y_range]
2421
+ # Hide all channels and show only the background
2422
+ self.channel_visible = [False] * 4
2423
+ self.is_pan_preview = True
2369
2424
 
2370
- if (new_xlim[0] <= self.original_xlim[0] or
2371
- new_xlim[1] >= self.original_xlim[1] or
2372
- new_ylim[0] <= self.original_ylim[0] or
2373
- new_ylim[1] >= self.original_ylim[1]):
2374
- self.ax.set_xlim(self.original_xlim)
2375
- self.ax.set_ylim(self.original_ylim)
2376
- else:
2377
- self.ax.set_xlim(new_xlim)
2378
- self.ax.set_ylim(new_ylim)
2425
+ # Update display to show only background
2426
+ self.update_display_pan_mode()
2427
+ else:
2428
+ self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
2379
2429
 
2380
- self.zoom_changed = False # Flag that zoom has changed
2381
-
2382
- if not hasattr(self, 'zoom_changed'):
2383
- self.zoom_changed = False
2384
-
2385
- self.canvas.draw()
2386
-
2387
- elif self.pan_mode:
2388
- self.panning = True
2389
- self.pan_start = (event.xdata, event.ydata)
2390
- self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
2391
-
2392
- # Store current channel visibility state
2393
- self.pre_pan_channel_state = self.channel_visible.copy()
2394
-
2395
- # Create static background from currently visible channels
2396
- self.create_pan_background()
2397
-
2398
- # Hide all channels and show only the background
2399
- self.channel_visible = [False] * 4
2400
- self.is_pan_preview = True
2401
-
2402
- # Update display to show only background
2403
- self.update_display_pan_mode()
2404
2430
 
2405
2431
  elif self.brush_mode:
2406
2432
  if event.inaxes != self.ax:
@@ -2408,13 +2434,20 @@ class ImageViewerWindow(QMainWindow):
2408
2434
 
2409
2435
  if event.button == 1 or event.button == 3:
2410
2436
  x, y = int(event.xdata), int(event.ydata)
2437
+ # Get current zoom to preserve it
2438
+
2439
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2440
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2441
+
2411
2442
 
2412
2443
  if event.button == 1 and self.can:
2444
+ self.update_display(preserve_zoom = (current_xlim, current_ylim))
2413
2445
  self.handle_can(x, y)
2414
2446
  return
2415
2447
 
2416
2448
  if event.button == 3:
2417
2449
  self.erase = True
2450
+ self.update_display(preserve_zoom = (current_xlim, current_ylim))
2418
2451
  else:
2419
2452
  self.erase = False
2420
2453
 
@@ -2426,10 +2459,6 @@ class ImageViewerWindow(QMainWindow):
2426
2459
  else:
2427
2460
  channel = 2
2428
2461
 
2429
- # Get current zoom to preserve it
2430
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2431
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2432
-
2433
2462
  # Paint at initial position
2434
2463
  self.paint_at_position(x, y, self.erase, channel)
2435
2464
 
@@ -2441,11 +2470,22 @@ class ImageViewerWindow(QMainWindow):
2441
2470
 
2442
2471
  # No need to hide other channels or track restore_channels
2443
2472
  self.restore_channels = []
2444
-
2445
- self.update_display(preserve_zoom=(current_xlim, current_ylim), begin_paint=True)
2473
+
2474
+ if self.static_background is None:
2475
+ if self.machine_window is not None:
2476
+ self.update_display(preserve_zoom = (current_xlim, current_ylim))
2477
+ elif not self.erase:
2478
+ self.temp_chan = channel
2479
+ self.channel_data[4] = self.channel_data[channel]
2480
+ self.min_max[4] = copy.deepcopy(self.min_max[channel])
2481
+ self.channel_brightness[4] = copy.deepcopy(self.channel_brightness[channel])
2482
+ self.load_channel(channel, np.zeros_like(self.channel_data[channel]), data = True, preserve_zoom = (current_xlim, current_ylim), begin_paint = True)
2483
+ self.channel_visible[4] = True
2484
+ self.static_background = self.canvas.copy_from_bbox(self.ax.bbox)
2485
+
2446
2486
  self.update_display_slice_optimized(channel, preserve_zoom=(current_xlim, current_ylim))
2447
2487
 
2448
- elif event.button == 3: # Right click (for context menu)
2488
+ elif not self.zoom_mode and event.button == 3: # Right click (for context menu)
2449
2489
  self.create_context_menu(event)
2450
2490
 
2451
2491
  elif event.button == 1: # Left click
@@ -2461,7 +2501,10 @@ class ImageViewerWindow(QMainWindow):
2461
2501
  if erase:
2462
2502
  val = 0
2463
2503
  elif self.machine_window is None:
2464
- val = 255
2504
+ try:
2505
+ val = max(255, self.min_max[4][1])
2506
+ except:
2507
+ val = 255
2465
2508
  elif self.foreground:
2466
2509
  val = 1
2467
2510
  else:
@@ -2498,7 +2541,10 @@ class ImageViewerWindow(QMainWindow):
2498
2541
  if erase:
2499
2542
  val = 0
2500
2543
  elif machine_window is None:
2501
- val = 255
2544
+ try:
2545
+ val = max(255, self.min_max[4][1])
2546
+ except:
2547
+ val = 255
2502
2548
  elif foreground:
2503
2549
  val = 1
2504
2550
  else:
@@ -2536,6 +2582,9 @@ class ImageViewerWindow(QMainWindow):
2536
2582
 
2537
2583
  def handle_can(self, x, y):
2538
2584
 
2585
+ # Update the channel data
2586
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2587
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2539
2588
 
2540
2589
  if self.threed:
2541
2590
  ref = copy.deepcopy(self.channel_data[self.active_channel])
@@ -2559,9 +2608,8 @@ class ImageViewerWindow(QMainWindow):
2559
2608
 
2560
2609
  # Add this mask to the original slice
2561
2610
  the_slice = the_slice | fill_mask # Use logical OR to add the filled region
2562
-
2563
- # Update the channel data
2564
- self.load_channel(self.active_channel, the_slice, True)
2611
+
2612
+ self.load_channel(self.active_channel, the_slice, True, preserve_zoom = (current_xlim, current_ylim))
2565
2613
  else:
2566
2614
 
2567
2615
  ref = copy.deepcopy(self.channel_data[self.active_channel])
@@ -2589,7 +2637,7 @@ class ImageViewerWindow(QMainWindow):
2589
2637
 
2590
2638
  # Update the channel data
2591
2639
  self.channel_data[self.active_channel][self.current_slice] = the_slice
2592
- self.load_channel(self.active_channel, self.channel_data[self.active_channel], True)
2640
+ self.load_channel(self.active_channel, self.channel_data[self.active_channel], True, preserve_zoom = (current_xlim, current_ylim))
2593
2641
 
2594
2642
 
2595
2643
 
@@ -2601,7 +2649,7 @@ class ImageViewerWindow(QMainWindow):
2601
2649
 
2602
2650
  current_time = time.time()
2603
2651
 
2604
- if self.selection_start and not self.selecting and not self.pan_mode and not self.zoom_mode and not self.brush_mode:
2652
+ if self.selection_start and not self.selecting and not self.pan_mode and not self.brush_mode:
2605
2653
  if (abs(event.xdata - self.selection_start[0]) > 1 or
2606
2654
  abs(event.ydata - self.selection_start[1]) > 1):
2607
2655
  self.selecting = True
@@ -2632,6 +2680,7 @@ class ImageViewerWindow(QMainWindow):
2632
2680
  self.canvas.blit(self.ax.bbox)
2633
2681
 
2634
2682
  elif self.panning and self.pan_start is not None:
2683
+
2635
2684
  # Calculate the movement
2636
2685
  dx = event.xdata - self.pan_start[0]
2637
2686
  dy = event.ydata - self.pan_start[1]
@@ -2727,7 +2776,6 @@ class ImageViewerWindow(QMainWindow):
2727
2776
  def end_paint_session(self):
2728
2777
  """Clean up after paint session."""
2729
2778
  self.paint_session_active = False
2730
- self.paint_background = None
2731
2779
  self.last_paint_pos = None
2732
2780
 
2733
2781
  def paint_worker_loop(self):
@@ -2960,6 +3008,7 @@ class ImageViewerWindow(QMainWindow):
2960
3008
 
2961
3009
  def update_display_pan_mode(self):
2962
3010
  """Lightweight display update for pan preview mode"""
3011
+
2963
3012
  if self.is_pan_preview and self.pan_background_image is not None:
2964
3013
  # Clear and setup axes
2965
3014
  self.ax.clear()
@@ -3007,26 +3056,18 @@ class ImageViewerWindow(QMainWindow):
3007
3056
  if z1 == z2 == self.current_slice:
3008
3057
  self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
3009
3058
 
3059
+ self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
3060
+
3010
3061
  self.canvas.draw_idle()
3011
3062
 
3012
3063
  def on_mouse_release(self, event):
3013
- """Handle mouse release events - OPTIMIZED VERSION."""
3064
+ """Handle mouse release events"""
3014
3065
  if self.pan_mode:
3015
- # Get current view limits before restoring channels
3016
- current_xlim = self.ax.get_xlim()
3017
- current_ylim = self.ax.get_ylim()
3018
3066
 
3019
3067
  self.panning = False
3020
3068
  self.pan_start = None
3021
3069
  self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
3022
3070
 
3023
- # Restore previously visible channels
3024
- self.channel_visible = self.pre_pan_channel_state.copy()
3025
- self.is_pan_preview = False
3026
- self.pan_background = None
3027
-
3028
- # Update display with preserved zoom at new position
3029
- self.update_display(preserve_zoom=(current_xlim, current_ylim))
3030
3071
  elif event.button == 1: # Left button release
3031
3072
  if self.selecting and self.selection_rect is not None:
3032
3073
  # Get the rectangle bounds
@@ -3034,9 +3075,20 @@ class ImageViewerWindow(QMainWindow):
3034
3075
  y0 = min(self.selection_start[1], event.ydata)
3035
3076
  width = abs(event.xdata - self.selection_start[0])
3036
3077
  height = abs(event.ydata - self.selection_start[1])
3078
+ shift_pressed = 'shift' in event.modifiers
3079
+
3080
+ if shift_pressed or self.zoom_mode: #Optional targeted zoom
3081
+
3082
+ self.ax.set_xlim([x0, x0 + width])
3083
+ self.ax.set_ylim([y0 + height, y0])
3084
+
3085
+ self.zoom_changed = True # Flag that zoom has changed
3086
+
3087
+ if not hasattr(self, 'zoom_changed'):
3088
+ self.zoom_changed = False
3037
3089
 
3038
3090
  # Get current slice data for active channel
3039
- if self.channel_data[self.active_channel] is not None:
3091
+ elif self.channel_data[self.active_channel] is not None:
3040
3092
  data = self.channel_data[self.active_channel][self.current_slice]
3041
3093
 
3042
3094
  # Convert coordinates to array indices
@@ -3102,32 +3154,85 @@ class ImageViewerWindow(QMainWindow):
3102
3154
  self.selection_rect = None
3103
3155
  self.canvas.draw()
3104
3156
 
3105
- # OPTIMIZED: Handle brush mode cleanup with paint session management
3106
- if self.brush_mode and hasattr(self, 'painting') and self.painting:
3107
- self.painting = False
3108
-
3109
- # Restore hidden channels
3110
- try:
3111
- for i in self.restore_channels:
3112
- self.channel_visible[i] = True
3113
- self.restore_channels = []
3114
- except:
3115
- pass
3157
+ elif self.zoom_mode:
3158
+ # Handle zoom mode press
3159
+ if self.original_xlim is None:
3160
+ self.original_xlim = self.ax.get_xlim()
3161
+ #print(self.original_xlim)
3162
+ self.original_ylim = self.ax.get_ylim()
3163
+ #print(self.original_ylim)
3116
3164
 
3117
- # OPTIMIZED: End paint session and ensure all operations complete
3118
- self.end_paint_session()
3165
+ current_xlim = self.ax.get_xlim()
3166
+ current_ylim = self.ax.get_ylim()
3167
+ xdata = event.xdata
3168
+ ydata = event.ydata
3119
3169
 
3120
- # OPTIMIZED: Stop timer and process any pending paint operations
3121
- if hasattr(self, 'paint_timer'):
3122
- self.paint_timer.stop()
3123
- if hasattr(self, 'pending_paint_update') and self.pending_paint_update:
3124
- self.flush_paint_updates()
3170
+ if event.button == 1: # Left click - zoom in
3171
+ x_range = (current_xlim[1] - current_xlim[0]) / 4
3172
+ y_range = (current_ylim[1] - current_ylim[0]) / 4
3173
+
3174
+ self.ax.set_xlim([xdata - x_range, xdata + x_range])
3175
+ self.ax.set_ylim([ydata - y_range, ydata + y_range])
3176
+
3177
+ self.zoom_changed = True # Flag that zoom has changed
3178
+
3179
+ if not hasattr(self, 'zoom_changed'):
3180
+ self.zoom_changed = False
3181
+
3182
+ elif event.button == 3: # Right click - zoom out
3183
+ x_range = (current_xlim[1] - current_xlim[0])
3184
+ y_range = (current_ylim[1] - current_ylim[0])
3185
+
3186
+ new_xlim = [xdata - x_range, xdata + x_range]
3187
+ new_ylim = [ydata - y_range, ydata + y_range]
3188
+
3189
+ if (new_xlim[0] <= self.original_xlim[0] or
3190
+ new_xlim[1] >= self.original_xlim[1] or
3191
+ new_ylim[0] <= self.original_ylim[0] or
3192
+ new_ylim[1] >= self.original_ylim[1]):
3193
+ self.ax.set_xlim(self.original_xlim)
3194
+ self.ax.set_ylim(self.original_ylim)
3195
+ else:
3196
+ self.ax.set_xlim(new_xlim)
3197
+ self.ax.set_ylim(new_ylim)
3198
+
3199
+ self.zoom_changed = False # Flag that zoom has changed
3200
+
3201
+ if not hasattr(self, 'zoom_changed'):
3202
+ self.zoom_changed = False
3125
3203
 
3126
- # Get current zoom and do final display update
3127
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3128
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3129
- self.update_display(preserve_zoom=(current_xlim, current_ylim))
3204
+ self.canvas.draw()
3130
3205
 
3206
+ # Handle brush mode cleanup with paint session management
3207
+ if self.brush_mode and hasattr(self, 'painting') and self.painting:
3208
+ self.painting = False
3209
+
3210
+ if self.erase:
3211
+ # Restore hidden channels
3212
+ try:
3213
+ for i in self.restore_channels:
3214
+ self.channel_visible[i] = True
3215
+ self.restore_channels = []
3216
+ except:
3217
+ pass
3218
+
3219
+ self.end_paint_session()
3220
+
3221
+ # OPTIMIZED: Stop timer and process any pending paint operations
3222
+ if hasattr(self, 'paint_timer'):
3223
+ self.paint_timer.stop()
3224
+ if hasattr(self, 'pending_paint_update') and self.pending_paint_update:
3225
+ self.flush_paint_updates()
3226
+
3227
+ self.static_background = None
3228
+
3229
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3230
+
3231
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3232
+
3233
+ self.update_display(preserve_zoom = (current_xlim, current_ylim))
3234
+
3235
+
3131
3236
 
3132
3237
  def highlight_value_in_tables(self, clicked_value):
3133
3238
  """Helper method to find and highlight a value in both tables."""
@@ -3381,7 +3486,7 @@ class ImageViewerWindow(QMainWindow):
3381
3486
  # Create Load submenu
3382
3487
  load_menu = file_menu.addMenu("Load")
3383
3488
  network_load = load_menu.addAction("Load Network3D Object")
3384
- network_load.triggered.connect(self.load_from_network_obj)
3489
+ network_load.triggered.connect(lambda: self.load_from_network_obj(None))
3385
3490
  for i in range(4):
3386
3491
  load_action = load_menu.addAction(f"Load {self.channel_names[i]}")
3387
3492
  load_action.triggered.connect(lambda checked, ch=i: self.load_channel(ch))
@@ -3396,6 +3501,8 @@ class ImageViewerWindow(QMainWindow):
3396
3501
  load_action.triggered.connect(lambda: self.load_misc('Node Centroids'))
3397
3502
  load_action = misc_menu.addAction("Load Edge Centroids")
3398
3503
  load_action.triggered.connect(lambda: self.load_misc('Edge Centroids'))
3504
+ load_action = misc_menu.addAction("Load Node Communities")
3505
+ load_action.triggered.connect(lambda: self.load_misc('Communities'))
3399
3506
  load_action = misc_menu.addAction("Merge Nodes")
3400
3507
  load_action.triggered.connect(lambda: self.load_misc('Merge Nodes'))
3401
3508
  load_action = misc_menu.addAction("Merge Node IDs from Images")
@@ -3413,7 +3520,7 @@ class ImageViewerWindow(QMainWindow):
3413
3520
  partition_action.triggered.connect(self.show_partition_dialog)
3414
3521
  com_identity_action = network_menu.addAction("Identity Makeup of Network Communities (and UMAP)")
3415
3522
  com_identity_action.triggered.connect(self.handle_com_id)
3416
- com_neighbor_action = network_menu.addAction("Convert Network Communities into Neighborhoods?")
3523
+ com_neighbor_action = network_menu.addAction("Convert Network Communities into Neighborhoods? (Also Returns Compositional Heatmaps)")
3417
3524
  com_neighbor_action.triggered.connect(self.handle_com_neighbor)
3418
3525
  com_cell_action = network_menu.addAction("Create Communities Based on Cuboidal Proximity Cells?")
3419
3526
  com_cell_action.triggered.connect(self.handle_com_cell)
@@ -3432,6 +3539,8 @@ class ImageViewerWindow(QMainWindow):
3432
3539
  ripley_action.triggered.connect(self.show_ripley_dialog)
3433
3540
  heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
3434
3541
  heatmap_action.triggered.connect(self.show_heatmap_dialog)
3542
+ nearneigh_action = stats_menu.addAction("Average Nearest Neighbors")
3543
+ nearneigh_action.triggered.connect(self.show_nearneigh_dialog)
3435
3544
  vol_action = stats_menu.addAction("Calculate Volumes")
3436
3545
  vol_action.triggered.connect(self.volumes)
3437
3546
  rad_action = stats_menu.addAction("Calculate Radii")
@@ -3535,14 +3644,29 @@ class ImageViewerWindow(QMainWindow):
3535
3644
  shuffle_action.triggered.connect(self.show_shuffle_dialog)
3536
3645
  arbitrary_action = image_menu.addAction("Select Objects")
3537
3646
  arbitrary_action.triggered.connect(self.show_arbitrary_dialog)
3538
- show3d_action = image_menu.addAction("Show 3D (Napari)")
3647
+ show3d_action = image_menu.addAction("Show 3D (Requires Napari)")
3539
3648
  show3d_action.triggered.connect(self.show3d_dialog)
3649
+ cellpose_action = image_menu.addAction("Cellpose (Requires Cellpose GUI installed)")
3650
+ cellpose_action.triggered.connect(self.open_cellpose)
3540
3651
 
3541
3652
  # Help
3542
3653
 
3543
3654
  help_button = menubar.addAction("Help")
3544
3655
  help_button.triggered.connect(self.help_me)
3545
3656
 
3657
+ def open_cellpose(self):
3658
+
3659
+ try:
3660
+
3661
+ from . import cellpose_manager
3662
+ self.cellpose_launcher = cellpose_manager.CellposeGUILauncher(parent_widget=self)
3663
+
3664
+ self.cellpose_launcher.launch_cellpose_gui()
3665
+
3666
+ except:
3667
+ pass
3668
+
3669
+
3546
3670
  def help_me(self):
3547
3671
 
3548
3672
  import webbrowser
@@ -3692,94 +3816,103 @@ class ImageViewerWindow(QMainWindow):
3692
3816
 
3693
3817
 
3694
3818
  def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None):
3695
- """
3696
- Format dictionary or list data for display in upper right table.
3697
-
3698
- Args:
3699
- data: Dictionary with keys and single/multiple values, or a list of values
3700
- metric: String for the key/index column header
3701
- value: String or list of strings for value column headers (used for dictionaries only)
3702
- title: Optional custom title for the tab
3703
- """
3704
- def convert_to_numeric(val):
3705
- """Helper function to convert strings to numeric types when possible"""
3706
- if isinstance(val, str):
3707
- try:
3708
- # First try converting to int
3709
- if '.' not in val:
3710
- return int(val)
3711
- # If that fails or if there's a decimal point, try float
3712
- return float(val)
3713
- except ValueError:
3714
- return val
3715
- return val
3716
-
3717
- try:
3718
-
3719
- if isinstance(data, (list, tuple, np.ndarray)):
3720
- # Handle list input - create single column DataFrame
3721
- df = pd.DataFrame({
3722
- metric: [convert_to_numeric(val) for val in data]
3723
- })
3724
-
3725
- # Format floating point numbers
3726
- df[metric] = df[metric].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
3727
-
3728
- else: # Dictionary input
3729
- # Get sample value to determine structure
3730
- sample_value = next(iter(data.values()))
3731
- is_multi_value = isinstance(sample_value, (list, tuple, np.ndarray))
3732
-
3733
- if is_multi_value:
3734
- # Handle multi-value case
3735
- if isinstance(value, str):
3736
- # If single string provided for multi-values, generate numbered headers
3737
- n_cols = len(sample_value)
3738
- value_headers = [f"{value}_{i+1}" for i in range(n_cols)]
3739
- else:
3740
- # Use provided list of headers
3741
- value_headers = value
3742
- if len(value_headers) != len(sample_value):
3743
- raise ValueError("Number of headers must match number of values per key")
3744
-
3745
- # Create lists for each column
3746
- dict_data = {metric: list(data.keys())}
3747
- for i, header in enumerate(value_headers):
3748
- # Convert values to numeric when possible before adding to DataFrame
3749
- dict_data[header] = [convert_to_numeric(data[key][i]) for key in data.keys()]
3750
-
3751
- df = pd.DataFrame(dict_data)
3752
-
3753
- # Format floating point numbers in all value columns
3754
- for header in value_headers:
3755
- df[header] = df[header].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
3756
-
3757
- else:
3758
- # Single-value case
3759
- df = pd.DataFrame({
3760
- metric: data.keys(),
3761
- value: [convert_to_numeric(val) for val in data.values()]
3762
- })
3763
-
3764
- # Format floating point numbers
3765
- df[value] = df[value].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
3766
-
3767
- # Create new table
3768
- table = CustomTableView(self)
3769
- table.setModel(PandasModel(df))
3770
-
3771
- # Add to tabbed widget
3772
- if title is None:
3773
- self.tabbed_data.add_table(f"{metric} Analysis", table)
3774
- else:
3775
- self.tabbed_data.add_table(f"{title}", table)
3776
-
3777
- # Adjust column widths to content
3778
- for column in range(table.model().columnCount(None)):
3779
- table.resizeColumnToContents(column)
3780
-
3781
- except:
3782
- pass
3819
+ """
3820
+ Format dictionary or list data for display in upper right table.
3821
+
3822
+ Args:
3823
+ data: Dictionary with keys and single/multiple values, or a list of values
3824
+ metric: String for the key/index column header
3825
+ value: String or list of strings for value column headers (used for dictionaries only)
3826
+ title: Optional custom title for the tab
3827
+ """
3828
+ def convert_to_numeric(val):
3829
+ """Helper function to convert strings to numeric types when possible"""
3830
+ if isinstance(val, str):
3831
+ try:
3832
+ # First try converting to int
3833
+ if '.' not in val:
3834
+ return int(val)
3835
+ # If that fails or if there's a decimal point, try float
3836
+ return float(val)
3837
+ except ValueError:
3838
+ return val
3839
+ return val
3840
+
3841
+ def format_number(x):
3842
+ """Smart formatting that removes trailing zeros"""
3843
+ if not isinstance(x, (float, np.float64)):
3844
+ return str(x)
3845
+
3846
+ # Use more decimal places, then strip trailing zeros
3847
+ formatted = f"{x:.8f}".rstrip('0').rstrip('.')
3848
+ return formatted if formatted else "0"
3849
+
3850
+ try:
3851
+
3852
+ if isinstance(data, (list, tuple, np.ndarray)):
3853
+ # Handle list input - create single column DataFrame
3854
+ df = pd.DataFrame({
3855
+ metric: [convert_to_numeric(val) for val in data]
3856
+ })
3857
+
3858
+ # Format floating point numbers
3859
+ df[metric] = df[metric].apply(format_number)
3860
+
3861
+ else: # Dictionary input
3862
+ # Get sample value to determine structure
3863
+ sample_value = next(iter(data.values()))
3864
+ is_multi_value = isinstance(sample_value, (list, tuple, np.ndarray))
3865
+
3866
+ if is_multi_value:
3867
+ # Handle multi-value case
3868
+ if isinstance(value, str):
3869
+ # If single string provided for multi-values, generate numbered headers
3870
+ n_cols = len(sample_value)
3871
+ value_headers = [f"{value}_{i+1}" for i in range(n_cols)]
3872
+ else:
3873
+ # Use provided list of headers
3874
+ value_headers = value
3875
+ if len(value_headers) != len(sample_value):
3876
+ raise ValueError("Number of headers must match number of values per key")
3877
+
3878
+ # Create lists for each column
3879
+ dict_data = {metric: list(data.keys())}
3880
+ for i, header in enumerate(value_headers):
3881
+ # Convert values to numeric when possible before adding to DataFrame
3882
+ dict_data[header] = [convert_to_numeric(data[key][i]) for key in data.keys()]
3883
+
3884
+ df = pd.DataFrame(dict_data)
3885
+
3886
+ # Format floating point numbers in all value columns
3887
+ for header in value_headers:
3888
+ df[header] = df[header].apply(format_number)
3889
+
3890
+ else:
3891
+ # Single-value case
3892
+ df = pd.DataFrame({
3893
+ metric: data.keys(),
3894
+ value: [convert_to_numeric(val) for val in data.values()]
3895
+ })
3896
+
3897
+ # Format floating point numbers
3898
+ df[value] = df[value].apply(format_number)
3899
+
3900
+ # Create new table
3901
+ table = CustomTableView(self)
3902
+ table.setModel(PandasModel(df))
3903
+
3904
+ # Add to tabbed widget
3905
+ if title is None:
3906
+ self.tabbed_data.add_table(f"{metric} Analysis", table)
3907
+ else:
3908
+ self.tabbed_data.add_table(f"{title}", table)
3909
+
3910
+ # Adjust column widths to content
3911
+ for column in range(table.model().columnCount(None)):
3912
+ table.resizeColumnToContents(column)
3913
+
3914
+ except:
3915
+ pass
3783
3916
 
3784
3917
  def show_merge_node_id_dialog(self):
3785
3918
 
@@ -4091,6 +4224,14 @@ class ImageViewerWindow(QMainWindow):
4091
4224
  self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
4092
4225
  except Exception as e:
4093
4226
  print(f"Error loading edge centroid table: {e}")
4227
+ elif sort == 'Communities':
4228
+ my_network.load_communities(file_path = filename)
4229
+
4230
+ if hasattr(my_network, 'communities') and my_network.communities is not None:
4231
+ try:
4232
+ self.format_for_upperright_table(my_network.communities, 'NodeID', 'Identity', 'Node Communities')
4233
+ except Exception as e:
4234
+ print(f"Error loading edge centroid table: {e}")
4094
4235
 
4095
4236
 
4096
4237
  except Exception as e:
@@ -4162,14 +4303,19 @@ class ImageViewerWindow(QMainWindow):
4162
4303
 
4163
4304
 
4164
4305
  # Modify load_from_network_obj method
4165
- def load_from_network_obj(self):
4306
+ def load_from_network_obj(self, directory = None):
4166
4307
  try:
4167
- directory = QFileDialog.getExistingDirectory(
4168
- self,
4169
- f"Select Directory for Network3D Object",
4170
- "",
4171
- QFileDialog.Option.ShowDirsOnly
4172
- )
4308
+
4309
+ if directory is None:
4310
+
4311
+ directory = QFileDialog.getExistingDirectory(
4312
+ self,
4313
+ f"Select Directory for Network3D Object",
4314
+ "",
4315
+ QFileDialog.Option.ShowDirsOnly
4316
+ )
4317
+
4318
+ self.last_load = directory
4173
4319
 
4174
4320
  if directory != "":
4175
4321
 
@@ -4391,7 +4537,7 @@ class ImageViewerWindow(QMainWindow):
4391
4537
  # Update button appearances to show active channel
4392
4538
  for i, btn in enumerate(self.channel_buttons):
4393
4539
  if i == index and btn.isEnabled():
4394
- btn.setStyleSheet("background-color: lightblue;")
4540
+ btn.setStyleSheet("font-weight: bold; color: yellow;")
4395
4541
  else:
4396
4542
  btn.setStyleSheet("")
4397
4543
 
@@ -4456,7 +4602,7 @@ class ImageViewerWindow(QMainWindow):
4456
4602
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
4457
4603
  return msg.exec() == QMessageBox.StandardButton.Yes
4458
4604
 
4459
- def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
4605
+ def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False):
4460
4606
  """Load a channel and enable active channel selection if needed."""
4461
4607
 
4462
4608
  try:
@@ -4507,6 +4653,9 @@ class ImageViewerWindow(QMainWindow):
4507
4653
 
4508
4654
  else:
4509
4655
  self.channel_data[channel_index] = channel_data
4656
+ if channel_data is None:
4657
+ self.channel_buttons[channel_index].setEnabled(False)
4658
+ self.delete_buttons[channel_index].setEnabled(False)
4510
4659
 
4511
4660
  if len(self.channel_data[channel_index].shape) == 2: # handle 2d data
4512
4661
  self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
@@ -4549,15 +4698,15 @@ class ImageViewerWindow(QMainWindow):
4549
4698
  self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
4550
4699
  break
4551
4700
 
4552
-
4553
- if channel_index == 0:
4554
- my_network.nodes = self.channel_data[channel_index]
4555
- elif channel_index == 1:
4556
- my_network.edges = self.channel_data[channel_index]
4557
- elif channel_index == 2:
4558
- my_network.network_overlay = self.channel_data[channel_index]
4559
- elif channel_index == 3:
4560
- my_network.id_overlay = self.channel_data[channel_index]
4701
+ if not begin_paint:
4702
+ if channel_index == 0:
4703
+ my_network.nodes = self.channel_data[channel_index]
4704
+ elif channel_index == 1:
4705
+ my_network.edges = self.channel_data[channel_index]
4706
+ elif channel_index == 2:
4707
+ my_network.network_overlay = self.channel_data[channel_index]
4708
+ elif channel_index == 3:
4709
+ my_network.id_overlay = self.channel_data[channel_index]
4561
4710
 
4562
4711
  # Enable the channel button
4563
4712
  self.channel_buttons[channel_index].setEnabled(True)
@@ -4619,9 +4768,12 @@ class ImageViewerWindow(QMainWindow):
4619
4768
  self.shape = self.channel_data[channel_index].shape
4620
4769
 
4621
4770
  self.img_height, self.img_width = self.shape[1], self.shape[2]
4771
+ self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
4772
+ #print(self.original_xlim)
4622
4773
 
4774
+ if not end_paint:
4623
4775
 
4624
- self.update_display(reset_resize = reset_resize)
4776
+ self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
4625
4777
 
4626
4778
 
4627
4779
 
@@ -4754,6 +4906,8 @@ class ImageViewerWindow(QMainWindow):
4754
4906
  # Call appropriate save method
4755
4907
  if asbool:
4756
4908
  my_network.dump(parent_dir=parent_dir, name=new_folder_name)
4909
+ self.last_saved = parent_dir
4910
+ self.last_save_name = new_folder_name
4757
4911
  else:
4758
4912
  my_network.dump(name='my_network')
4759
4913
 
@@ -4815,10 +4969,11 @@ class ImageViewerWindow(QMainWindow):
4815
4969
  # Store current zoom settings before toggling
4816
4970
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
4817
4971
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
4818
-
4972
+
4819
4973
  self.channel_visible[channel_index] = self.channel_buttons[channel_index].isChecked()
4820
4974
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
4821
4975
 
4976
+
4822
4977
 
4823
4978
  def update_slice(self):
4824
4979
  """Queue a slice update when slider moves."""
@@ -4856,24 +5011,60 @@ class ImageViewerWindow(QMainWindow):
4856
5011
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
4857
5012
  # Convert slider values (0-100) to data values (0-1)
4858
5013
  min_val, max_val = values
4859
- self.channel_brightness[channel_index]['min'] = min_val / 65535 #Accomodate 32 bit data?
5014
+ self.channel_brightness[channel_index]['min'] = min_val / 65535
4860
5015
  self.channel_brightness[channel_index]['max'] = max_val / 65535
4861
5016
  self.update_display(preserve_zoom = (current_xlim, current_ylim))
4862
5017
 
4863
5018
 
4864
5019
 
4865
5020
 
4866
- def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False, begin_paint = False):
5021
+ def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False):
4867
5022
  """Update the display with currently visible channels and highlight overlay."""
4868
5023
 
4869
5024
  try:
4870
5025
 
4871
- if begin_paint:
4872
- # Store/update the static background with current zoom level
4873
- self.static_background = self.canvas.copy_from_bbox(self.ax.bbox)
5026
+ self.figure.clear()
4874
5027
 
5028
+ if self.pan_background_image is not None:
5029
+ # Restore previously visible channels
5030
+ self.channel_visible = self.pre_pan_channel_state.copy()
5031
+ self.is_pan_preview = False
5032
+ self.pan_background_image = None
4875
5033
 
4876
- self.figure.clear()
5034
+ if self.machine_window is not None:
5035
+ if self.machine_window.segmentation_worker is not None:
5036
+ self.machine_window.segmentation_worker.resume()
5037
+
5038
+ if self.static_background is not None:
5039
+
5040
+ # Restore hidden channels
5041
+ try:
5042
+ for i in self.restore_channels:
5043
+ self.channel_visible[i] = True
5044
+ self.restore_channels = []
5045
+ except:
5046
+ pass
5047
+
5048
+ self.end_paint_session()
5049
+
5050
+ # OPTIMIZED: Stop timer and process any pending paint operations
5051
+ if hasattr(self, 'paint_timer'):
5052
+ self.paint_timer.stop()
5053
+ if hasattr(self, 'pending_paint_update') and self.pending_paint_update:
5054
+ self.flush_paint_updates()
5055
+
5056
+ self.static_background = None
5057
+
5058
+ if self.machine_window is None:
5059
+
5060
+ try:
5061
+
5062
+ self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
5063
+ self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
5064
+ self.channel_data[4] = None
5065
+ self.channel_visible[4] = False
5066
+ except:
5067
+ pass
4877
5068
 
4878
5069
  # Get active channels and their dimensions
4879
5070
  active_channels = [i for i in range(4) if self.channel_data[i] is not None]
@@ -4947,9 +5138,6 @@ class ImageViewerWindow(QMainWindow):
4947
5138
  else:
4948
5139
  vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
4949
5140
  vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
4950
-
4951
-
4952
-
4953
5141
 
4954
5142
  # Normalize the image safely
4955
5143
  if vmin == vmax:
@@ -5022,9 +5210,39 @@ class ImageViewerWindow(QMainWindow):
5022
5210
  vmax=2, # Important: set vmax to 2 to accommodate both values
5023
5211
  alpha=0.5)
5024
5212
 
5213
+ if self.channel_data[4] is not None:
5025
5214
 
5215
+ highlight_slice = self.channel_data[4][self.current_slice]
5216
+ img_min = self.min_max[4][0]
5217
+ img_max = self.min_max[4][1]
5218
+
5219
+ # Calculate vmin and vmax, ensuring we don't get a zero range
5220
+ if img_min == img_max:
5221
+ vmin = img_min
5222
+ vmax = img_min + 1
5223
+ else:
5224
+ vmin = img_min + (img_max - img_min) * self.channel_brightness[4]['min']
5225
+ vmax = img_min + (img_max - img_min) * self.channel_brightness[4]['max']
5226
+
5227
+ # Normalize the image safely
5228
+ if vmin == vmax:
5229
+ normalized_image = np.zeros_like(highlight_slice)
5230
+ else:
5231
+ normalized_image = np.clip((highlight_slice - vmin) / (vmax - vmin), 0, 1)
5232
+
5233
+ color = base_colors[self.temp_chan]
5234
+ custom_cmap = LinearSegmentedColormap.from_list(
5235
+ f'custom_{4}',
5236
+ [(0,0,0,0), (*color,1)]
5237
+ )
5238
+
5239
+
5240
+ self.ax.imshow(normalized_image,
5241
+ alpha=0.7,
5242
+ cmap=custom_cmap,
5243
+ vmin=0,
5244
+ vmax=1)
5026
5245
 
5027
-
5028
5246
  # Style the axes
5029
5247
  self.ax.set_xlabel('X')
5030
5248
  self.ax.set_ylabel('Y')
@@ -5212,6 +5430,10 @@ class ImageViewerWindow(QMainWindow):
5212
5430
  dialog = HeatmapDialog(self)
5213
5431
  dialog.exec()
5214
5432
 
5433
+ def show_nearneigh_dialog(self):
5434
+ dialog = NearNeighDialog(self)
5435
+ dialog.exec()
5436
+
5215
5437
  def show_random_dialog(self):
5216
5438
  dialog = RandomDialog(self)
5217
5439
  dialog.exec()
@@ -5408,6 +5630,9 @@ class CustomTableView(QTableView):
5408
5630
  save_menu = context_menu.addMenu("Save As")
5409
5631
  save_csv = save_menu.addAction("CSV")
5410
5632
  save_excel = save_menu.addAction("Excel")
5633
+ close_action = context_menu.addAction("Close All")
5634
+
5635
+ close_action.triggered.connect(self.close_all)
5411
5636
 
5412
5637
  # Connect the actions
5413
5638
  save_csv.triggered.connect(lambda: self.save_table_as('csv'))
@@ -5615,7 +5840,9 @@ class CustomTableView(QTableView):
5615
5840
  except Exception as e:
5616
5841
  print(f"Error setting new network: {e}")
5617
5842
 
5843
+ def close_all(self):
5618
5844
 
5845
+ self.parent.tabbed_data.clear_all_tabs()
5619
5846
 
5620
5847
  def handle_find_action(self, row, column, value):
5621
5848
  """Handle the Find action for bottom tables."""
@@ -6914,10 +7141,7 @@ class NetShowDialog(QDialog):
6914
7141
 
6915
7142
  def show_network(self):
6916
7143
  # Get parameters and run analysis
6917
- if my_network.communities is None:
6918
- self.parent().show_partition_dialog()
6919
- if my_network.communities is None:
6920
- return
7144
+
6921
7145
  geo = self.geo_layout.isChecked()
6922
7146
  if geo:
6923
7147
  if my_network.node_centroids is None:
@@ -6928,6 +7152,13 @@ class NetShowDialog(QDialog):
6928
7152
 
6929
7153
  weighted = self.weighted.isChecked()
6930
7154
 
7155
+ if accepted_mode == 1:
7156
+
7157
+ if my_network.communities is None:
7158
+ self.parent().show_partition_dialog()
7159
+ if my_network.communities is None:
7160
+ return
7161
+
6931
7162
  try:
6932
7163
  if accepted_mode == 0:
6933
7164
  my_network.show_network(geometric=geo, directory = directory)
@@ -6967,7 +7198,7 @@ class PartitionDialog(QDialog):
6967
7198
  # stats checkbox (default True)
6968
7199
  self.stats = QPushButton("Stats")
6969
7200
  self.stats.setCheckable(True)
6970
- self.stats.setChecked(True)
7201
+ self.stats.setChecked(False)
6971
7202
  layout.addRow("Community Stats:", self.stats)
6972
7203
 
6973
7204
  self.seed = QLineEdit("")
@@ -6994,7 +7225,7 @@ class PartitionDialog(QDialog):
6994
7225
 
6995
7226
  try:
6996
7227
  stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats, seed = seed)
6997
- print(f"Discovered communities: {my_network.communities}")
7228
+ #print(f"Discovered communities: {my_network.communities}")
6998
7229
 
6999
7230
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
7000
7231
 
@@ -7033,6 +7264,14 @@ class ComIdDialog(QDialog):
7033
7264
  self.label.setChecked(False)
7034
7265
  layout.addRow("If using above - label UMAP points?:", self.label)
7035
7266
 
7267
+ self.limit = QLineEdit("")
7268
+ layout.addRow("Min Community Size for UMAP (Smaller communities will be ignored in graph, does not apply if empty)", self.limit)
7269
+
7270
+ # weighted checkbox (default True)
7271
+ self.proportional = QPushButton("Robust")
7272
+ self.proportional.setCheckable(True)
7273
+ self.proportional.setChecked(False)
7274
+ layout.addRow("Return Node Type Distribution Robust UMAP (ie, communities will show how much they overrepresent a node type rather than just their proportional composition):", self.proportional)
7036
7275
 
7037
7276
  # Add Run button
7038
7277
  run_button = QPushButton("Get Community ID Info")
@@ -7056,6 +7295,9 @@ class ComIdDialog(QDialog):
7056
7295
 
7057
7296
  umap = self.umap.isChecked()
7058
7297
  label = self.label.isChecked()
7298
+ proportional = self.proportional.isChecked()
7299
+ limit = int(self.limit.text()) if self.limit.text().strip() else 0
7300
+
7059
7301
 
7060
7302
  if mode == 1:
7061
7303
 
@@ -7065,7 +7307,7 @@ class ComIdDialog(QDialog):
7065
7307
 
7066
7308
  else:
7067
7309
 
7068
- info, names = my_network.community_id_info_per_com(umap = umap, label = label)
7310
+ info, names = my_network.community_id_info_per_com(umap = umap, label = label, limit = limit, proportional = proportional)
7069
7311
 
7070
7312
  self.parent().format_for_upperright_table(info, 'Community', names, 'Average of Community Makeup')
7071
7313
 
@@ -7073,6 +7315,9 @@ class ComIdDialog(QDialog):
7073
7315
 
7074
7316
  except Exception as e:
7075
7317
 
7318
+ import traceback
7319
+ print(traceback.format_exc())
7320
+
7076
7321
  print(f"Error: {e}")
7077
7322
 
7078
7323
 
@@ -7082,12 +7327,13 @@ class ComNeighborDialog(QDialog):
7082
7327
  def __init__(self, parent=None):
7083
7328
 
7084
7329
  super().__init__(parent)
7085
- self.setWindowTitle("Reassign Communities Based on Identity Similarity?")
7330
+ self.setWindowTitle("Reassign Communities Based on Identity Similarity? (Note this alters communities outside of this function)")
7086
7331
  self.setModal(True)
7087
7332
 
7088
7333
  layout = QFormLayout(self)
7089
7334
 
7090
- self.neighborcount = QLineEdit("5")
7335
+ self.neighborcount = QLineEdit("")
7336
+ self.neighborcount.setPlaceholderText("KMeans Only. Empty = auto-predict (between 1 and 20)")
7091
7337
  layout.addRow("Num Neighborhoods:", self.neighborcount)
7092
7338
 
7093
7339
  self.seed = QLineEdit("")
@@ -7096,8 +7342,19 @@ class ComNeighborDialog(QDialog):
7096
7342
  self.limit = QLineEdit("")
7097
7343
  layout.addRow("Min Community Size to be grouped (Smaller communities will be placed in neighborhood 0 - does not apply if empty)", self.limit)
7098
7344
 
7345
+ # weighted checkbox (default True)
7346
+ self.proportional = QPushButton("Robust")
7347
+ self.proportional.setCheckable(True)
7348
+ self.proportional.setChecked(False)
7349
+ layout.addRow("Return Node Type Distribution Robust Heatmaps (ie, will give two more heatmaps that are not beholden to the total number of nodes of each type, representing which structures are overrepresented in a network):", self.proportional)
7350
+
7351
+ self.mode = QComboBox()
7352
+ self.mode.addItems(["KMeans (Better at simplifying data)", "DBSCAN (Better at capturing anomalies)"])
7353
+ self.mode.setCurrentIndex(0)
7354
+ layout.addRow("Mode", self.mode)
7355
+
7099
7356
  # Add Run button
7100
- run_button = QPushButton("Get Communities")
7357
+ run_button = QPushButton("Get Neighborhoods")
7101
7358
  run_button.clicked.connect(self.run)
7102
7359
  layout.addWidget(run_button)
7103
7360
 
@@ -7114,20 +7371,29 @@ class ComNeighborDialog(QDialog):
7114
7371
  if my_network.communities is None:
7115
7372
  return
7116
7373
 
7374
+ mode = self.mode.currentIndex()
7375
+
7117
7376
  seed = float(self.seed.text()) if self.seed.text().strip() else 42
7118
7377
 
7119
7378
  limit = int(self.limit.text()) if self.limit.text().strip() else None
7120
7379
 
7380
+ proportional = self.proportional.isChecked()
7121
7381
 
7122
- neighborcount = int(self.neighborcount.text()) if self.neighborcount.text().strip() else 5
7382
+ neighborcount = int(self.neighborcount.text()) if self.neighborcount.text().strip() else None
7123
7383
 
7124
7384
  if self.parent().prev_coms is None:
7125
7385
 
7126
7386
  self.parent().prev_coms = copy.deepcopy(my_network.communities)
7127
- my_network.assign_neighborhoods(seed, neighborcount, limit = limit)
7387
+ len_dict, matrixes, id_set = my_network.assign_neighborhoods(seed, neighborcount, limit = limit, proportional = proportional, mode = mode)
7128
7388
  else:
7129
- my_network.assign_neighborhoods(seed, neighborcount, limit = limit, prev_coms = self.parent().prev_coms)
7389
+ len_dict, matrixes, id_set = my_network.assign_neighborhoods(seed, neighborcount, limit = limit, prev_coms = self.parent().prev_coms, proportional = proportional, mode = mode)
7130
7390
 
7391
+
7392
+ for i, matrix in enumerate(matrixes):
7393
+ self.parent().format_for_upperright_table(matrix, 'NeighborhoodID', id_set, title = f'Neighborhood Heatmap {i + 1}')
7394
+
7395
+
7396
+ self.parent().format_for_upperright_table(len_dict, 'NeighborhoodID', 'Proportion of Total Nodes', title = 'Neighborhood Counts')
7131
7397
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'NeighborhoodID', title = 'Neighborhood Partition')
7132
7398
 
7133
7399
  print("Neighborhoods have been assigned to communities based on similarity")
@@ -7136,6 +7402,9 @@ class ComNeighborDialog(QDialog):
7136
7402
 
7137
7403
  except Exception as e:
7138
7404
 
7405
+ import traceback
7406
+ print(traceback.format_exc())
7407
+
7139
7408
  print(f"Error assigning neighborhoods: {e}")
7140
7409
 
7141
7410
  class ComCellDialog(QDialog):
@@ -7158,7 +7427,7 @@ class ComCellDialog(QDialog):
7158
7427
  layout.addRow("z scale:", self.z_scale)
7159
7428
 
7160
7429
  # Add Run button
7161
- run_button = QPushButton("Get Neighborhoods (Note this overwrites current communities - save your coms first)")
7430
+ run_button = QPushButton("Get Communities (Note this overwrites current communities - save your coms first)")
7162
7431
  run_button.clicked.connect(self.run)
7163
7432
  layout.addWidget(run_button)
7164
7433
 
@@ -7269,6 +7538,174 @@ class DegreeDistDialog(QDialog):
7269
7538
  except Exception as e:
7270
7539
  print(f"An error occurred: {e}")
7271
7540
 
7541
+ class NearNeighDialog(QDialog):
7542
+ def __init__(self, parent=None):
7543
+ super().__init__(parent)
7544
+ self.setWindowTitle(f"Nearest Neighborhood Averages (Using Centroids)")
7545
+ self.setModal(True)
7546
+
7547
+ # Main layout
7548
+ main_layout = QVBoxLayout(self)
7549
+
7550
+ # Identities group box (only if node_identities exists)
7551
+ identities_group = QGroupBox("Identities")
7552
+ identities_layout = QFormLayout(identities_group)
7553
+
7554
+ if my_network.node_identities is not None:
7555
+
7556
+ self.root = QComboBox()
7557
+ self.root.addItems(list(set(my_network.node_identities.values())))
7558
+ self.root.setCurrentIndex(0)
7559
+ identities_layout.addRow("Root Identity to Search for Neighbor's IDs?", self.root)
7560
+
7561
+ self.targ = QComboBox()
7562
+ neighs = list(set(my_network.node_identities.values()))
7563
+ neighs.append("All Others (Excluding Self)")
7564
+ self.targ.addItems(neighs)
7565
+ self.targ.setCurrentIndex(0)
7566
+ identities_layout.addRow("Neighbor Identities to Search For?", self.targ)
7567
+ else:
7568
+ self.root = None
7569
+ self.targ = None
7570
+
7571
+ self.num = QLineEdit("1")
7572
+ identities_layout.addRow("Number of Nearest Neighbors to Evaluate Per Node?:", self.num)
7573
+
7574
+
7575
+ main_layout.addWidget(identities_group)
7576
+
7577
+
7578
+ # Optional Heatmap group box
7579
+ heatmap_group = QGroupBox("Optional Heatmap")
7580
+ heatmap_layout = QFormLayout(heatmap_group)
7581
+
7582
+ self.map = QPushButton("(If getting distribution): Generate Heatmap?")
7583
+ self.map.setCheckable(True)
7584
+ self.map.setChecked(False)
7585
+ heatmap_layout.addRow("Heatmap:", self.map)
7586
+
7587
+ self.threed = QPushButton("(For above): Return 3D map? (uncheck for 2D): ")
7588
+ self.threed.setCheckable(True)
7589
+ self.threed.setChecked(True)
7590
+ heatmap_layout.addRow("3D:", self.threed)
7591
+
7592
+ self.numpy = QPushButton("(For heatmap): Return image overlay instead of graph? (Goes in Overlay 2): ")
7593
+ self.numpy.setCheckable(True)
7594
+ self.numpy.setChecked(False)
7595
+ self.numpy.clicked.connect(self.toggle_map)
7596
+ heatmap_layout.addRow("Overlay:", self.numpy)
7597
+
7598
+ main_layout.addWidget(heatmap_group)
7599
+
7600
+ # Get Distribution group box
7601
+ distribution_group = QGroupBox("Get Distribution")
7602
+ distribution_layout = QVBoxLayout(distribution_group)
7603
+
7604
+ run_button = QPushButton("Get Average Nearest Neighbor (Plus Distribution)")
7605
+ run_button.clicked.connect(self.run)
7606
+ distribution_layout.addWidget(run_button)
7607
+
7608
+ main_layout.addWidget(distribution_group)
7609
+
7610
+ # Get All Averages group box (only if node_identities exists)
7611
+ if my_network.node_identities is not None:
7612
+ averages_group = QGroupBox("Get All Averages")
7613
+ averages_layout = QVBoxLayout(averages_group)
7614
+
7615
+ run_button2 = QPushButton("Get Average Nearest All ID Combinations (No Distribution, No Heatmap)")
7616
+ run_button2.clicked.connect(self.run2)
7617
+ averages_layout.addWidget(run_button2)
7618
+
7619
+ main_layout.addWidget(averages_group)
7620
+
7621
+ def toggle_map(self):
7622
+
7623
+ if self.numpy.isChecked():
7624
+
7625
+ if not self.map.isChecked():
7626
+
7627
+ self.map.click()
7628
+
7629
+ def run(self):
7630
+
7631
+ try:
7632
+
7633
+ try:
7634
+ root = self.root.currentText()
7635
+ except:
7636
+ root = None
7637
+ try:
7638
+ targ = self.targ.currentText()
7639
+ except:
7640
+ targ = None
7641
+
7642
+ heatmap = self.map.isChecked()
7643
+ threed = self.threed.isChecked()
7644
+ numpy = self.numpy.isChecked()
7645
+ num = int(self.num.text()) if self.num.text().strip() else 1
7646
+
7647
+ if root is not None and targ is not None:
7648
+ title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
7649
+ header = f"Shortest Distance to Closest {num} {targ}(s)"
7650
+ header2 = f"{root} Node ID"
7651
+ else:
7652
+ title = f"Nearest {num} Neighbor(s) Distance Between Nodes"
7653
+ header = f"Shortest Distance to Closest {num} Nodes"
7654
+ header2 = "Root Node ID"
7655
+
7656
+ if my_network.node_centroids is None:
7657
+ self.parent().show_centroid_dialog()
7658
+ if my_network.node_centroids is None:
7659
+ return
7660
+
7661
+ if not numpy:
7662
+ avg, output = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed)
7663
+ else:
7664
+ avg, output, overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True)
7665
+ self.parent().load_channel(3, overlay, data = True)
7666
+
7667
+ self.parent().format_for_upperright_table([avg], metric = f'Avg {title}', title = f'Avg {title}')
7668
+ self.parent().format_for_upperright_table(output, header2, header, title = title)
7669
+
7670
+ self.accept()
7671
+
7672
+ except Exception as e:
7673
+ import traceback
7674
+ print(traceback.format_exc())
7675
+
7676
+ print(f"Error: {e}")
7677
+
7678
+ def run2(self):
7679
+
7680
+ try:
7681
+
7682
+ available = list(set(my_network.node_identities.values()))
7683
+
7684
+ num = int(self.num.text()) if self.num.text().strip() else 1
7685
+
7686
+ output_dict = {}
7687
+
7688
+ while len(available) > 1:
7689
+
7690
+ root = available[0]
7691
+
7692
+ for targ in available:
7693
+
7694
+ avg, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
7695
+
7696
+ output_dict[f"{root} vs {targ}"] = avg
7697
+
7698
+ del available[0]
7699
+
7700
+ self.parent().format_for_upperright_table(output_dict, "ID Combo", "Avg Distance to Nearest", title = "Average Distance to Nearest Neighbors for All ID Combos")
7701
+
7702
+ self.accept()
7703
+
7704
+ except Exception as e:
7705
+
7706
+ print(f"Error: {e}")
7707
+
7708
+
7272
7709
  class NeighborIdentityDialog(QDialog):
7273
7710
 
7274
7711
  def __init__(self, parent=None):
@@ -7455,8 +7892,7 @@ class RipleyDialog(QDialog):
7455
7892
  "Error:",
7456
7893
  f"Failed to preform cluster analysis: {str(e)}"
7457
7894
  )
7458
- import traceback
7459
- print(traceback.format_exc())
7895
+
7460
7896
  print(f"Error: {e}")
7461
7897
 
7462
7898
  class HeatmapDialog(QDialog):
@@ -7479,6 +7915,11 @@ class HeatmapDialog(QDialog):
7479
7915
  self.is3d.setChecked(True)
7480
7916
  layout.addRow("Use 3D Plot (uncheck for 2D)?:", self.is3d)
7481
7917
 
7918
+ self.numpy = QPushButton("(For heatmap): Return image overlay instead of graph? (Goes in Overlay 2): ")
7919
+ self.numpy.setCheckable(True)
7920
+ self.numpy.setChecked(False)
7921
+ layout.addRow("Overlay:", self.numpy)
7922
+
7482
7923
 
7483
7924
  # Add Run button
7484
7925
  run_button = QPushButton("Run")
@@ -7487,25 +7928,40 @@ class HeatmapDialog(QDialog):
7487
7928
 
7488
7929
  def run(self):
7489
7930
 
7490
- nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
7931
+ try:
7491
7932
 
7492
- is3d = self.is3d.isChecked()
7933
+ nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
7934
+
7935
+ is3d = self.is3d.isChecked()
7493
7936
 
7494
7937
 
7495
- if my_network.communities is None:
7496
- if my_network.network is not None:
7497
- self.parent().show_partition_dialog()
7498
- else:
7499
- self.parent().handle_com_cell()
7500
7938
  if my_network.communities is None:
7501
- return
7939
+ if my_network.network is not None:
7940
+ self.parent().show_partition_dialog()
7941
+ else:
7942
+ self.parent().handle_com_cell()
7943
+ if my_network.communities is None:
7944
+ return
7502
7945
 
7503
- heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
7946
+ numpy = self.numpy.isChecked()
7504
7947
 
7505
- self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
7948
+ if not numpy:
7506
7949
 
7507
- self.accept()
7950
+ heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
7508
7951
 
7952
+ else:
7953
+
7954
+ heat_dict, overlay = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d, numpy = True)
7955
+ self.parent().load_channel(3, overlay, data = True)
7956
+
7957
+
7958
+ self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
7959
+
7960
+ self.accept()
7961
+
7962
+ except Exception as e:
7963
+
7964
+ print(f"Error: {e}")
7509
7965
 
7510
7966
 
7511
7967
 
@@ -7597,9 +8053,9 @@ class RandNodeDialog(QDialog):
7597
8053
 
7598
8054
  # Calculate shape if not provided
7599
8055
  max_coords = centroid_points.max(axis=0)
7600
- max_shape = tuple(max_coord + 1 for max_coord in max_coords)
8056
+ max_shape = tuple(max_coord for max_coord in max_coords)
7601
8057
  min_coords = centroid_points.min(axis=0)
7602
- min_shape = tuple(min_coord + 1 for min_coord in min_coords)
8058
+ min_shape = tuple(min_coord for min_coord in min_coords)
7603
8059
  bounds = (min_shape, max_shape)
7604
8060
  else:
7605
8061
  mask = n3d.binarize(self.parent().channel_data[mode - 1])
@@ -7749,7 +8205,7 @@ class DegreeDialog(QDialog):
7749
8205
 
7750
8206
  # Add mode selection dropdown
7751
8207
  self.mode_selector = QComboBox()
7752
- self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis... slower)"])
8208
+ self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis)", "Create Heatmap of Degrees"])
7753
8209
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
7754
8210
  layout.addRow("Execution Mode:", self.mode_selector)
7755
8211
 
@@ -7770,6 +8226,14 @@ class DegreeDialog(QDialog):
7770
8226
 
7771
8227
  accepted_mode = self.mode_selector.currentIndex()
7772
8228
 
8229
+ if accepted_mode == 3:
8230
+ degree_dict, overlay = my_network.get_degrees(heatmap = True)
8231
+ self.parent().format_for_upperright_table(degree_dict, 'Node ID', 'Degree', title = 'Degrees of nodes')
8232
+ self.parent().load_channel(3, channel_data = overlay, data = True)
8233
+ self.accept()
8234
+ return
8235
+
8236
+
7773
8237
  try:
7774
8238
  down_factor = float(self.down_factor.text()) if self.down_factor.text() else 1
7775
8239
  except ValueError:
@@ -8417,6 +8881,12 @@ class BinarizeDialog(QDialog):
8417
8881
 
8418
8882
  layout = QFormLayout(self)
8419
8883
 
8884
+ # Add mode selection dropdown
8885
+ self.mode = QComboBox()
8886
+ self.mode.addItems(["Total Binarize", "Predict Foreground"])
8887
+ self.mode.setCurrentIndex(0) # Default to Mode 1
8888
+ layout.addRow("Method:", self.mode)
8889
+
8420
8890
  # Add Run button
8421
8891
  run_button = QPushButton("Run Binarize")
8422
8892
  run_button.clicked.connect(self.run_binarize)
@@ -8431,11 +8901,19 @@ class BinarizeDialog(QDialog):
8431
8901
  if active_data is None:
8432
8902
  raise ValueError("No active image selected")
8433
8903
 
8904
+ mode = self.mode.currentIndex()
8905
+
8434
8906
  try:
8435
- # Call binarize method with parameters
8436
- result = n3d.binarize(
8437
- active_data
8438
- )
8907
+
8908
+ if mode == 0:
8909
+ # Call binarize method with parameters
8910
+ result = n3d.binarize(
8911
+ active_data
8912
+ )
8913
+ else:
8914
+ result = n3d.otsu_binarize(
8915
+ active_data, True
8916
+ )
8439
8917
 
8440
8918
  # Update both the display data and the network object
8441
8919
  self.parent().channel_data[self.parent().active_channel] = result
@@ -9104,12 +9582,16 @@ class MachineWindow(QMainWindow):
9104
9582
  if self.parent().brush_mode:
9105
9583
  self.parent().pan_button.setChecked(False)
9106
9584
  self.parent().zoom_button.setChecked(False)
9585
+ if self.parent().pan_mode:
9586
+ current_xlim = self.parent().ax.get_xlim()
9587
+ current_ylim = self.parent().ax.get_ylim()
9588
+ self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
9107
9589
  self.parent().pan_mode = False
9108
9590
  self.parent().zoom_mode = False
9109
9591
  self.parent().update_brush_cursor()
9110
9592
  else:
9111
- self.threed = False
9112
- self.can = False
9593
+ self.parent().threed = False
9594
+ self.parent().can = False
9113
9595
  self.parent().zoom_button.click()
9114
9596
 
9115
9597
  def silence_button(self):
@@ -9422,6 +9904,7 @@ class SegmentationWorker(QThread):
9422
9904
  self.machine_window = machine_window
9423
9905
  self.mem_lock = mem_lock
9424
9906
  self._stop = False
9907
+ self._paused = False # Add pause flag
9425
9908
  self.update_interval = 1 # Increased to 500ms
9426
9909
  self.chunks_since_update = 0
9427
9910
  self.chunks_per_update = 5 # Only update every 5 chunks
@@ -9431,6 +9914,23 @@ class SegmentationWorker(QThread):
9431
9914
  def stop(self):
9432
9915
  self._stop = True
9433
9916
 
9917
+ def pause(self):
9918
+ """Pause the segmentation worker"""
9919
+ self._paused = True
9920
+
9921
+ def resume(self):
9922
+ """Resume the segmentation worker"""
9923
+ self._paused = False
9924
+
9925
+ def is_paused(self):
9926
+ """Check if the worker is currently paused"""
9927
+ return self._paused
9928
+
9929
+ def _check_pause(self):
9930
+ """Check if paused and wait until resumed"""
9931
+ while self._paused and not self._stop:
9932
+ self.msleep(50) # Sleep for 50ms while paused
9933
+
9434
9934
  def get_poked(self):
9435
9935
  self.machine_window.poke_segmenter()
9436
9936
 
@@ -9447,6 +9947,11 @@ class SegmentationWorker(QThread):
9447
9947
 
9448
9948
  # Process the slice with chunked generator
9449
9949
  for foreground, background in self.segmenter.segment_slice_chunked(current_z):
9950
+ # Check for pause/stop before processing each chunk
9951
+ self._check_pause()
9952
+ if self._stop:
9953
+ break
9954
+
9450
9955
  if foreground == None and background == None:
9451
9956
  self.get_poked()
9452
9957
 
@@ -9471,6 +9976,8 @@ class SegmentationWorker(QThread):
9471
9976
  else:
9472
9977
  # Original 3D approach
9473
9978
  for foreground_coords, background_coords in self.segmenter.segment_volume_realtime(gpu=self.use_gpu):
9979
+ # Check for pause/stop before processing each chunk
9980
+ self._check_pause()
9474
9981
  if self._stop:
9475
9982
  break
9476
9983
 
@@ -9501,6 +10008,10 @@ class SegmentationWorker(QThread):
9501
10008
  # Modify the array directly
9502
10009
  self.overlay.fill(False)
9503
10010
  for z,y,x in foreground_coords:
10011
+ # Check for pause/stop during batch processing too
10012
+ self._check_pause()
10013
+ if self._stop:
10014
+ break
9504
10015
  self.overlay[z,y,x] = True
9505
10016
 
9506
10017
  self.finished.emit()
@@ -10274,7 +10785,7 @@ class CropDialog(QDialog):
10274
10785
  try:
10275
10786
 
10276
10787
  super().__init__(parent)
10277
- self.setWindowTitle("Crop Image?")
10788
+ self.setWindowTitle("Crop Image (Will transpose any centroids)?")
10278
10789
  self.setModal(True)
10279
10790
 
10280
10791
  layout = QFormLayout(self)
@@ -10330,10 +10841,70 @@ class CropDialog(QDialog):
10330
10841
 
10331
10842
  self.parent().load_channel(i, array, data = True)
10332
10843
 
10844
+ print("Transposing centroids...")
10845
+
10846
+ try:
10847
+
10848
+ if my_network.node_centroids is not None:
10849
+ nodes = list(my_network.node_centroids.keys())
10850
+ centroids = np.array(list(my_network.node_centroids.values()))
10851
+
10852
+ # Transform all at once
10853
+ transformed = centroids - np.array([zmin, ymin, xmin])
10854
+ transformed = transformed.astype(int)
10855
+
10856
+ # Boolean mask for valid coordinates
10857
+ valid_mask = ((transformed >= 0) &
10858
+ (transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
10859
+
10860
+ # Rebuild dictionary with only valid entries
10861
+ my_network.node_centroids = {
10862
+ nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
10863
+ for i in range(len(nodes)) if valid_mask[i]
10864
+ }
10865
+
10866
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
10867
+
10868
+ except Exception as e:
10869
+
10870
+ print(f"Error transposing node centroids: {e}")
10871
+
10872
+ try:
10873
+
10874
+ if my_network.edge_centroids is not None:
10875
+
10876
+ if my_network.edge_centroids is not None:
10877
+ nodes = list(my_network.edge_centroids.keys())
10878
+ centroids = np.array(list(my_network.edge_centroids.values()))
10879
+
10880
+ # Transform all at once
10881
+ transformed = centroids - np.array([zmin, ymin, xmin])
10882
+ transformed = transformed.astype(int)
10883
+
10884
+ # Boolean mask for valid coordinates
10885
+ valid_mask = ((transformed >= 0) &
10886
+ (transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
10887
+
10888
+ # Rebuild dictionary with only valid entries
10889
+ my_network.edge_centroids = {
10890
+ nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
10891
+ for i in range(len(nodes)) if valid_mask[i]
10892
+ }
10893
+
10894
+ self.parent().format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
10895
+
10896
+ except Exception as e:
10897
+
10898
+ print(f"Error transposing edge centroids: {e}")
10899
+
10900
+
10333
10901
  self.accept()
10334
10902
 
10335
10903
  except Exception as e:
10336
10904
 
10905
+ import traceback
10906
+ print(traceback.format_exc())
10907
+
10337
10908
  print(f"Error cropping: {e}")
10338
10909
 
10339
10910
 
@@ -12003,7 +12574,10 @@ class ProxDialog(QDialog):
12003
12574
  # Speed Up Options Group
12004
12575
  speedup_group = QGroupBox("Speed Up Options")
12005
12576
  speedup_layout = QFormLayout(speedup_group)
12006
-
12577
+
12578
+ self.max_neighbors = QLineEdit("")
12579
+ speedup_layout.addRow("(If using centroids): Max number of closest neighbors each node can connect to? Further neighbors within the radius will be ignored if a value is passed here. (Can be good to simplify dense networks)", self.max_neighbors)
12580
+
12007
12581
  self.fastdil = QPushButton("Fast Dilate")
12008
12582
  self.fastdil.setCheckable(True)
12009
12583
  self.fastdil.setChecked(False)
@@ -12055,6 +12629,11 @@ class ProxDialog(QDialog):
12055
12629
  except ValueError:
12056
12630
  search = None
12057
12631
 
12632
+ try:
12633
+ max_neighbors = int(self.max_neighbors.text()) if self.max_neighbors.text() else None
12634
+ except:
12635
+ max_neighbors = None
12636
+
12058
12637
  overlays = self.overlays.isChecked()
12059
12638
  fastdil = self.fastdil.isChecked()
12060
12639
 
@@ -12093,10 +12672,10 @@ class ProxDialog(QDialog):
12093
12672
  return
12094
12673
 
12095
12674
  if populate:
12096
- my_network.nodes = my_network.kd_network(distance = search, targets = targets, make_array = True)
12675
+ my_network.nodes = my_network.kd_network(distance = search, targets = targets, make_array = True, max_neighbors = max_neighbors)
12097
12676
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
12098
12677
  else:
12099
- my_network.kd_network(distance = search, targets = targets)
12678
+ my_network.kd_network(distance = search, targets = targets, max_neighbors = max_neighbors)
12100
12679
 
12101
12680
  if directory is not None:
12102
12681
  my_network.dump(directory = directory)