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