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.
- nettracer3d/cellpose_manager.py +161 -0
- nettracer3d/community_extractor.py +97 -20
- nettracer3d/neighborhoods.py +617 -81
- nettracer3d/nettracer.py +282 -74
- nettracer3d/nettracer_gui.py +860 -281
- nettracer3d/network_analysis.py +222 -230
- nettracer3d/node_draw.py +22 -12
- nettracer3d/proximity.py +254 -30
- nettracer3d-0.8.2.dist-info/METADATA +117 -0
- nettracer3d-0.8.2.dist-info/RECORD +24 -0
- nettracer3d-0.8.0.dist-info/METADATA +0 -83
- nettracer3d-0.8.0.dist-info/RECORD +0 -23
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.2.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.2.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.2.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -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] *
|
|
47
|
-
self.channel_visible = [False] *
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2361
|
-
|
|
2415
|
+
# Store current channel visibility state
|
|
2416
|
+
self.pre_pan_channel_state = self.channel_visible.copy()
|
|
2362
2417
|
|
|
2363
|
-
|
|
2364
|
-
|
|
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
|
-
|
|
2368
|
-
|
|
2421
|
+
# Hide all channels and show only the background
|
|
2422
|
+
self.channel_visible = [False] * 4
|
|
2423
|
+
self.is_pan_preview = True
|
|
2369
2424
|
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3106
|
-
|
|
3107
|
-
self.
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
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
|
-
|
|
3118
|
-
self.
|
|
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
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
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
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
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("
|
|
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
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7931
|
+
try:
|
|
7491
7932
|
|
|
7492
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7946
|
+
numpy = self.numpy.isChecked()
|
|
7504
7947
|
|
|
7505
|
-
|
|
7948
|
+
if not numpy:
|
|
7506
7949
|
|
|
7507
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
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)
|