nettracer3d 0.8.1__py3-none-any.whl → 0.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- nettracer3d/cellpose_manager.py +161 -0
- nettracer3d/community_extractor.py +169 -23
- nettracer3d/neighborhoods.py +222 -23
- nettracer3d/nettracer.py +166 -68
- nettracer3d/nettracer_gui.py +584 -266
- nettracer3d/network_analysis.py +222 -230
- nettracer3d/proximity.py +191 -30
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/METADATA +44 -12
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/RECORD +13 -12
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.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):
|
|
2217
2270
|
|
|
2271
|
+
try:
|
|
2272
|
+
|
|
2273
|
+
if self.last_saved is None:
|
|
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)
|
|
@@ -3537,14 +3644,29 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3537
3644
|
shuffle_action.triggered.connect(self.show_shuffle_dialog)
|
|
3538
3645
|
arbitrary_action = image_menu.addAction("Select Objects")
|
|
3539
3646
|
arbitrary_action.triggered.connect(self.show_arbitrary_dialog)
|
|
3540
|
-
show3d_action = image_menu.addAction("Show 3D (Napari)")
|
|
3647
|
+
show3d_action = image_menu.addAction("Show 3D (Requires Napari)")
|
|
3541
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)
|
|
3542
3651
|
|
|
3543
3652
|
# Help
|
|
3544
3653
|
|
|
3545
3654
|
help_button = menubar.addAction("Help")
|
|
3546
3655
|
help_button.triggered.connect(self.help_me)
|
|
3547
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
|
+
|
|
3548
3670
|
def help_me(self):
|
|
3549
3671
|
|
|
3550
3672
|
import webbrowser
|
|
@@ -3694,94 +3816,103 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3694
3816
|
|
|
3695
3817
|
|
|
3696
3818
|
def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None):
|
|
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
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
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
|
|
3785
3916
|
|
|
3786
3917
|
def show_merge_node_id_dialog(self):
|
|
3787
3918
|
|
|
@@ -4093,6 +4224,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4093
4224
|
self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
|
|
4094
4225
|
except Exception as e:
|
|
4095
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}")
|
|
4096
4235
|
|
|
4097
4236
|
|
|
4098
4237
|
except Exception as e:
|
|
@@ -4164,14 +4303,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4164
4303
|
|
|
4165
4304
|
|
|
4166
4305
|
# Modify load_from_network_obj method
|
|
4167
|
-
def load_from_network_obj(self):
|
|
4306
|
+
def load_from_network_obj(self, directory = None):
|
|
4168
4307
|
try:
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
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
|
|
4175
4319
|
|
|
4176
4320
|
if directory != "":
|
|
4177
4321
|
|
|
@@ -4393,7 +4537,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4393
4537
|
# Update button appearances to show active channel
|
|
4394
4538
|
for i, btn in enumerate(self.channel_buttons):
|
|
4395
4539
|
if i == index and btn.isEnabled():
|
|
4396
|
-
btn.setStyleSheet("
|
|
4540
|
+
btn.setStyleSheet("font-weight: bold; color: yellow;")
|
|
4397
4541
|
else:
|
|
4398
4542
|
btn.setStyleSheet("")
|
|
4399
4543
|
|
|
@@ -4458,7 +4602,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4458
4602
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
4459
4603
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
4460
4604
|
|
|
4461
|
-
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):
|
|
4462
4606
|
"""Load a channel and enable active channel selection if needed."""
|
|
4463
4607
|
|
|
4464
4608
|
try:
|
|
@@ -4509,6 +4653,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4509
4653
|
|
|
4510
4654
|
else:
|
|
4511
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)
|
|
4512
4659
|
|
|
4513
4660
|
if len(self.channel_data[channel_index].shape) == 2: # handle 2d data
|
|
4514
4661
|
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
@@ -4551,15 +4698,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4551
4698
|
self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
|
|
4552
4699
|
break
|
|
4553
4700
|
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
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]
|
|
4563
4710
|
|
|
4564
4711
|
# Enable the channel button
|
|
4565
4712
|
self.channel_buttons[channel_index].setEnabled(True)
|
|
@@ -4621,9 +4768,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4621
4768
|
self.shape = self.channel_data[channel_index].shape
|
|
4622
4769
|
|
|
4623
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)
|
|
4624
4773
|
|
|
4774
|
+
if not end_paint:
|
|
4625
4775
|
|
|
4626
|
-
|
|
4776
|
+
self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
|
|
4627
4777
|
|
|
4628
4778
|
|
|
4629
4779
|
|
|
@@ -4756,6 +4906,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4756
4906
|
# Call appropriate save method
|
|
4757
4907
|
if asbool:
|
|
4758
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
|
|
4759
4911
|
else:
|
|
4760
4912
|
my_network.dump(name='my_network')
|
|
4761
4913
|
|
|
@@ -4817,10 +4969,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4817
4969
|
# Store current zoom settings before toggling
|
|
4818
4970
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
4819
4971
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
4820
|
-
|
|
4972
|
+
|
|
4821
4973
|
self.channel_visible[channel_index] = self.channel_buttons[channel_index].isChecked()
|
|
4822
4974
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
4823
4975
|
|
|
4976
|
+
|
|
4824
4977
|
|
|
4825
4978
|
def update_slice(self):
|
|
4826
4979
|
"""Queue a slice update when slider moves."""
|
|
@@ -4858,24 +5011,60 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4858
5011
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
4859
5012
|
# Convert slider values (0-100) to data values (0-1)
|
|
4860
5013
|
min_val, max_val = values
|
|
4861
|
-
self.channel_brightness[channel_index]['min'] = min_val / 65535
|
|
5014
|
+
self.channel_brightness[channel_index]['min'] = min_val / 65535
|
|
4862
5015
|
self.channel_brightness[channel_index]['max'] = max_val / 65535
|
|
4863
5016
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
4864
5017
|
|
|
4865
5018
|
|
|
4866
5019
|
|
|
4867
5020
|
|
|
4868
|
-
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):
|
|
4869
5022
|
"""Update the display with currently visible channels and highlight overlay."""
|
|
4870
5023
|
|
|
4871
5024
|
try:
|
|
4872
5025
|
|
|
4873
|
-
|
|
4874
|
-
# Store/update the static background with current zoom level
|
|
4875
|
-
self.static_background = self.canvas.copy_from_bbox(self.ax.bbox)
|
|
5026
|
+
self.figure.clear()
|
|
4876
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
|
|
4877
5033
|
|
|
4878
|
-
|
|
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
|
|
4879
5068
|
|
|
4880
5069
|
# Get active channels and their dimensions
|
|
4881
5070
|
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
@@ -4949,9 +5138,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4949
5138
|
else:
|
|
4950
5139
|
vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
|
|
4951
5140
|
vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
5141
|
|
|
4956
5142
|
# Normalize the image safely
|
|
4957
5143
|
if vmin == vmax:
|
|
@@ -5024,9 +5210,39 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5024
5210
|
vmax=2, # Important: set vmax to 2 to accommodate both values
|
|
5025
5211
|
alpha=0.5)
|
|
5026
5212
|
|
|
5213
|
+
if self.channel_data[4] is not None:
|
|
5027
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)
|
|
5028
5245
|
|
|
5029
|
-
|
|
5030
5246
|
# Style the axes
|
|
5031
5247
|
self.ax.set_xlabel('X')
|
|
5032
5248
|
self.ax.set_ylabel('Y')
|
|
@@ -5414,6 +5630,9 @@ class CustomTableView(QTableView):
|
|
|
5414
5630
|
save_menu = context_menu.addMenu("Save As")
|
|
5415
5631
|
save_csv = save_menu.addAction("CSV")
|
|
5416
5632
|
save_excel = save_menu.addAction("Excel")
|
|
5633
|
+
close_action = context_menu.addAction("Close All")
|
|
5634
|
+
|
|
5635
|
+
close_action.triggered.connect(self.close_all)
|
|
5417
5636
|
|
|
5418
5637
|
# Connect the actions
|
|
5419
5638
|
save_csv.triggered.connect(lambda: self.save_table_as('csv'))
|
|
@@ -5621,7 +5840,9 @@ class CustomTableView(QTableView):
|
|
|
5621
5840
|
except Exception as e:
|
|
5622
5841
|
print(f"Error setting new network: {e}")
|
|
5623
5842
|
|
|
5843
|
+
def close_all(self):
|
|
5624
5844
|
|
|
5845
|
+
self.parent.tabbed_data.clear_all_tabs()
|
|
5625
5846
|
|
|
5626
5847
|
def handle_find_action(self, row, column, value):
|
|
5627
5848
|
"""Handle the Find action for bottom tables."""
|
|
@@ -6920,10 +7141,7 @@ class NetShowDialog(QDialog):
|
|
|
6920
7141
|
|
|
6921
7142
|
def show_network(self):
|
|
6922
7143
|
# Get parameters and run analysis
|
|
6923
|
-
|
|
6924
|
-
self.parent().show_partition_dialog()
|
|
6925
|
-
if my_network.communities is None:
|
|
6926
|
-
return
|
|
7144
|
+
|
|
6927
7145
|
geo = self.geo_layout.isChecked()
|
|
6928
7146
|
if geo:
|
|
6929
7147
|
if my_network.node_centroids is None:
|
|
@@ -6934,6 +7152,13 @@ class NetShowDialog(QDialog):
|
|
|
6934
7152
|
|
|
6935
7153
|
weighted = self.weighted.isChecked()
|
|
6936
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
|
+
|
|
6937
7162
|
try:
|
|
6938
7163
|
if accepted_mode == 0:
|
|
6939
7164
|
my_network.show_network(geometric=geo, directory = directory)
|
|
@@ -6973,7 +7198,7 @@ class PartitionDialog(QDialog):
|
|
|
6973
7198
|
# stats checkbox (default True)
|
|
6974
7199
|
self.stats = QPushButton("Stats")
|
|
6975
7200
|
self.stats.setCheckable(True)
|
|
6976
|
-
self.stats.setChecked(
|
|
7201
|
+
self.stats.setChecked(False)
|
|
6977
7202
|
layout.addRow("Community Stats:", self.stats)
|
|
6978
7203
|
|
|
6979
7204
|
self.seed = QLineEdit("")
|
|
@@ -7000,7 +7225,7 @@ class PartitionDialog(QDialog):
|
|
|
7000
7225
|
|
|
7001
7226
|
try:
|
|
7002
7227
|
stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats, seed = seed)
|
|
7003
|
-
print(f"Discovered communities: {my_network.communities}")
|
|
7228
|
+
#print(f"Discovered communities: {my_network.communities}")
|
|
7004
7229
|
|
|
7005
7230
|
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
|
|
7006
7231
|
|
|
@@ -7039,6 +7264,14 @@ class ComIdDialog(QDialog):
|
|
|
7039
7264
|
self.label.setChecked(False)
|
|
7040
7265
|
layout.addRow("If using above - label UMAP points?:", self.label)
|
|
7041
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)
|
|
7042
7275
|
|
|
7043
7276
|
# Add Run button
|
|
7044
7277
|
run_button = QPushButton("Get Community ID Info")
|
|
@@ -7062,6 +7295,9 @@ class ComIdDialog(QDialog):
|
|
|
7062
7295
|
|
|
7063
7296
|
umap = self.umap.isChecked()
|
|
7064
7297
|
label = self.label.isChecked()
|
|
7298
|
+
proportional = self.proportional.isChecked()
|
|
7299
|
+
limit = int(self.limit.text()) if self.limit.text().strip() else 0
|
|
7300
|
+
|
|
7065
7301
|
|
|
7066
7302
|
if mode == 1:
|
|
7067
7303
|
|
|
@@ -7071,7 +7307,7 @@ class ComIdDialog(QDialog):
|
|
|
7071
7307
|
|
|
7072
7308
|
else:
|
|
7073
7309
|
|
|
7074
|
-
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)
|
|
7075
7311
|
|
|
7076
7312
|
self.parent().format_for_upperright_table(info, 'Community', names, 'Average of Community Makeup')
|
|
7077
7313
|
|
|
@@ -7079,6 +7315,9 @@ class ComIdDialog(QDialog):
|
|
|
7079
7315
|
|
|
7080
7316
|
except Exception as e:
|
|
7081
7317
|
|
|
7318
|
+
import traceback
|
|
7319
|
+
print(traceback.format_exc())
|
|
7320
|
+
|
|
7082
7321
|
print(f"Error: {e}")
|
|
7083
7322
|
|
|
7084
7323
|
|
|
@@ -7088,12 +7327,13 @@ class ComNeighborDialog(QDialog):
|
|
|
7088
7327
|
def __init__(self, parent=None):
|
|
7089
7328
|
|
|
7090
7329
|
super().__init__(parent)
|
|
7091
|
-
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)")
|
|
7092
7331
|
self.setModal(True)
|
|
7093
7332
|
|
|
7094
7333
|
layout = QFormLayout(self)
|
|
7095
7334
|
|
|
7096
|
-
self.neighborcount = QLineEdit("
|
|
7335
|
+
self.neighborcount = QLineEdit("")
|
|
7336
|
+
self.neighborcount.setPlaceholderText("KMeans Only. Empty = auto-predict (between 1 and 20)")
|
|
7097
7337
|
layout.addRow("Num Neighborhoods:", self.neighborcount)
|
|
7098
7338
|
|
|
7099
7339
|
self.seed = QLineEdit("")
|
|
@@ -7102,8 +7342,19 @@ class ComNeighborDialog(QDialog):
|
|
|
7102
7342
|
self.limit = QLineEdit("")
|
|
7103
7343
|
layout.addRow("Min Community Size to be grouped (Smaller communities will be placed in neighborhood 0 - does not apply if empty)", self.limit)
|
|
7104
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
|
+
|
|
7105
7356
|
# Add Run button
|
|
7106
|
-
run_button = QPushButton("Get
|
|
7357
|
+
run_button = QPushButton("Get Neighborhoods")
|
|
7107
7358
|
run_button.clicked.connect(self.run)
|
|
7108
7359
|
layout.addWidget(run_button)
|
|
7109
7360
|
|
|
@@ -7120,20 +7371,29 @@ class ComNeighborDialog(QDialog):
|
|
|
7120
7371
|
if my_network.communities is None:
|
|
7121
7372
|
return
|
|
7122
7373
|
|
|
7374
|
+
mode = self.mode.currentIndex()
|
|
7375
|
+
|
|
7123
7376
|
seed = float(self.seed.text()) if self.seed.text().strip() else 42
|
|
7124
7377
|
|
|
7125
7378
|
limit = int(self.limit.text()) if self.limit.text().strip() else None
|
|
7126
7379
|
|
|
7380
|
+
proportional = self.proportional.isChecked()
|
|
7127
7381
|
|
|
7128
|
-
neighborcount = int(self.neighborcount.text()) if self.neighborcount.text().strip() else
|
|
7382
|
+
neighborcount = int(self.neighborcount.text()) if self.neighborcount.text().strip() else None
|
|
7129
7383
|
|
|
7130
7384
|
if self.parent().prev_coms is None:
|
|
7131
7385
|
|
|
7132
7386
|
self.parent().prev_coms = copy.deepcopy(my_network.communities)
|
|
7133
|
-
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)
|
|
7134
7388
|
else:
|
|
7135
|
-
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)
|
|
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}')
|
|
7136
7394
|
|
|
7395
|
+
|
|
7396
|
+
self.parent().format_for_upperright_table(len_dict, 'NeighborhoodID', 'Proportion of Total Nodes', title = 'Neighborhood Counts')
|
|
7137
7397
|
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'NeighborhoodID', title = 'Neighborhood Partition')
|
|
7138
7398
|
|
|
7139
7399
|
print("Neighborhoods have been assigned to communities based on similarity")
|
|
@@ -7142,6 +7402,9 @@ class ComNeighborDialog(QDialog):
|
|
|
7142
7402
|
|
|
7143
7403
|
except Exception as e:
|
|
7144
7404
|
|
|
7405
|
+
import traceback
|
|
7406
|
+
print(traceback.format_exc())
|
|
7407
|
+
|
|
7145
7408
|
print(f"Error assigning neighborhoods: {e}")
|
|
7146
7409
|
|
|
7147
7410
|
class ComCellDialog(QDialog):
|
|
@@ -7164,7 +7427,7 @@ class ComCellDialog(QDialog):
|
|
|
7164
7427
|
layout.addRow("z scale:", self.z_scale)
|
|
7165
7428
|
|
|
7166
7429
|
# Add Run button
|
|
7167
|
-
run_button = QPushButton("Get
|
|
7430
|
+
run_button = QPushButton("Get Communities (Note this overwrites current communities - save your coms first)")
|
|
7168
7431
|
run_button.clicked.connect(self.run)
|
|
7169
7432
|
layout.addWidget(run_button)
|
|
7170
7433
|
|
|
@@ -7790,9 +8053,9 @@ class RandNodeDialog(QDialog):
|
|
|
7790
8053
|
|
|
7791
8054
|
# Calculate shape if not provided
|
|
7792
8055
|
max_coords = centroid_points.max(axis=0)
|
|
7793
|
-
max_shape = tuple(max_coord
|
|
8056
|
+
max_shape = tuple(max_coord for max_coord in max_coords)
|
|
7794
8057
|
min_coords = centroid_points.min(axis=0)
|
|
7795
|
-
min_shape = tuple(min_coord
|
|
8058
|
+
min_shape = tuple(min_coord for min_coord in min_coords)
|
|
7796
8059
|
bounds = (min_shape, max_shape)
|
|
7797
8060
|
else:
|
|
7798
8061
|
mask = n3d.binarize(self.parent().channel_data[mode - 1])
|
|
@@ -8618,6 +8881,12 @@ class BinarizeDialog(QDialog):
|
|
|
8618
8881
|
|
|
8619
8882
|
layout = QFormLayout(self)
|
|
8620
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
|
+
|
|
8621
8890
|
# Add Run button
|
|
8622
8891
|
run_button = QPushButton("Run Binarize")
|
|
8623
8892
|
run_button.clicked.connect(self.run_binarize)
|
|
@@ -8632,11 +8901,19 @@ class BinarizeDialog(QDialog):
|
|
|
8632
8901
|
if active_data is None:
|
|
8633
8902
|
raise ValueError("No active image selected")
|
|
8634
8903
|
|
|
8904
|
+
mode = self.mode.currentIndex()
|
|
8905
|
+
|
|
8635
8906
|
try:
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
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
|
+
)
|
|
8640
8917
|
|
|
8641
8918
|
# Update both the display data and the network object
|
|
8642
8919
|
self.parent().channel_data[self.parent().active_channel] = result
|
|
@@ -9305,12 +9582,16 @@ class MachineWindow(QMainWindow):
|
|
|
9305
9582
|
if self.parent().brush_mode:
|
|
9306
9583
|
self.parent().pan_button.setChecked(False)
|
|
9307
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))
|
|
9308
9589
|
self.parent().pan_mode = False
|
|
9309
9590
|
self.parent().zoom_mode = False
|
|
9310
9591
|
self.parent().update_brush_cursor()
|
|
9311
9592
|
else:
|
|
9312
|
-
self.threed = False
|
|
9313
|
-
self.can = False
|
|
9593
|
+
self.parent().threed = False
|
|
9594
|
+
self.parent().can = False
|
|
9314
9595
|
self.parent().zoom_button.click()
|
|
9315
9596
|
|
|
9316
9597
|
def silence_button(self):
|
|
@@ -9623,6 +9904,7 @@ class SegmentationWorker(QThread):
|
|
|
9623
9904
|
self.machine_window = machine_window
|
|
9624
9905
|
self.mem_lock = mem_lock
|
|
9625
9906
|
self._stop = False
|
|
9907
|
+
self._paused = False # Add pause flag
|
|
9626
9908
|
self.update_interval = 1 # Increased to 500ms
|
|
9627
9909
|
self.chunks_since_update = 0
|
|
9628
9910
|
self.chunks_per_update = 5 # Only update every 5 chunks
|
|
@@ -9632,6 +9914,23 @@ class SegmentationWorker(QThread):
|
|
|
9632
9914
|
def stop(self):
|
|
9633
9915
|
self._stop = True
|
|
9634
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
|
+
|
|
9635
9934
|
def get_poked(self):
|
|
9636
9935
|
self.machine_window.poke_segmenter()
|
|
9637
9936
|
|
|
@@ -9648,6 +9947,11 @@ class SegmentationWorker(QThread):
|
|
|
9648
9947
|
|
|
9649
9948
|
# Process the slice with chunked generator
|
|
9650
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
|
+
|
|
9651
9955
|
if foreground == None and background == None:
|
|
9652
9956
|
self.get_poked()
|
|
9653
9957
|
|
|
@@ -9672,6 +9976,8 @@ class SegmentationWorker(QThread):
|
|
|
9672
9976
|
else:
|
|
9673
9977
|
# Original 3D approach
|
|
9674
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()
|
|
9675
9981
|
if self._stop:
|
|
9676
9982
|
break
|
|
9677
9983
|
|
|
@@ -9702,6 +10008,10 @@ class SegmentationWorker(QThread):
|
|
|
9702
10008
|
# Modify the array directly
|
|
9703
10009
|
self.overlay.fill(False)
|
|
9704
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
|
|
9705
10015
|
self.overlay[z,y,x] = True
|
|
9706
10016
|
|
|
9707
10017
|
self.finished.emit()
|
|
@@ -12264,7 +12574,10 @@ class ProxDialog(QDialog):
|
|
|
12264
12574
|
# Speed Up Options Group
|
|
12265
12575
|
speedup_group = QGroupBox("Speed Up Options")
|
|
12266
12576
|
speedup_layout = QFormLayout(speedup_group)
|
|
12267
|
-
|
|
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
|
+
|
|
12268
12581
|
self.fastdil = QPushButton("Fast Dilate")
|
|
12269
12582
|
self.fastdil.setCheckable(True)
|
|
12270
12583
|
self.fastdil.setChecked(False)
|
|
@@ -12316,6 +12629,11 @@ class ProxDialog(QDialog):
|
|
|
12316
12629
|
except ValueError:
|
|
12317
12630
|
search = None
|
|
12318
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
|
+
|
|
12319
12637
|
overlays = self.overlays.isChecked()
|
|
12320
12638
|
fastdil = self.fastdil.isChecked()
|
|
12321
12639
|
|
|
@@ -12354,10 +12672,10 @@ class ProxDialog(QDialog):
|
|
|
12354
12672
|
return
|
|
12355
12673
|
|
|
12356
12674
|
if populate:
|
|
12357
|
-
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)
|
|
12358
12676
|
self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
|
|
12359
12677
|
else:
|
|
12360
|
-
my_network.kd_network(distance = search, targets = targets)
|
|
12678
|
+
my_network.kd_network(distance = search, targets = targets, max_neighbors = max_neighbors)
|
|
12361
12679
|
|
|
12362
12680
|
if directory is not None:
|
|
12363
12681
|
my_network.dump(directory = directory)
|