nettracer3d 1.1.1__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 +1745 -482
- 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.1.dist-info → nettracer3d-1.2.3.dist-info}/METADATA +5 -3
- nettracer3d-1.2.3.dist-info/RECORD +29 -0
- nettracer3d-1.1.1.dist-info/RECORD +0 -26
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.3.dist-info}/WHEEL +0 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.3.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.3.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-1.1.1.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
|
|
|
@@ -4894,6 +5189,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4894
5189
|
print(f"Error opening URL: {e}")
|
|
4895
5190
|
return False
|
|
4896
5191
|
|
|
5192
|
+
def start_tutorial(self):
|
|
5193
|
+
"""Open the tutorial selection dialog"""
|
|
5194
|
+
if not hasattr(self, 'tutorial_dialog'):
|
|
5195
|
+
self.tutorial_dialog = TutorialSelectionDialog(self)
|
|
5196
|
+
self.tutorial_dialog.show()
|
|
5197
|
+
self.tutorial_dialog.raise_()
|
|
5198
|
+
self.tutorial_dialog.activateWindow()
|
|
5199
|
+
|
|
4897
5200
|
|
|
4898
5201
|
def stats(self):
|
|
4899
5202
|
"""Method to get and display the network stats"""
|
|
@@ -4962,7 +5265,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4962
5265
|
|
|
4963
5266
|
|
|
4964
5267
|
|
|
4965
|
-
def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None, sort = True):
|
|
5268
|
+
def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None, sort = True, save = False):
|
|
4966
5269
|
"""
|
|
4967
5270
|
Format dictionary or list data for display in upper right table.
|
|
4968
5271
|
|
|
@@ -5058,15 +5361,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5058
5361
|
# Add to tabbed widget
|
|
5059
5362
|
if title is None:
|
|
5060
5363
|
self.tabbed_data.add_table(f"{metric} Analysis", table)
|
|
5364
|
+
#print(list(self.tabbed_data.tables.values())[-1].model()._data)
|
|
5365
|
+
#for reference, the above is how you access the data in the tabbed data viz
|
|
5061
5366
|
else:
|
|
5062
5367
|
self.tabbed_data.add_table(f"{title}", table)
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
5368
|
# Adjust column widths to content
|
|
5067
5369
|
for column in range(table.model().columnCount(None)):
|
|
5068
5370
|
table.resizeColumnToContents(column)
|
|
5069
5371
|
|
|
5372
|
+
if save:
|
|
5373
|
+
table.save_table_as('csv')
|
|
5070
5374
|
return df
|
|
5071
5375
|
|
|
5072
5376
|
except:
|
|
@@ -5117,12 +5421,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5117
5421
|
def show_calc_all_dialog(self):
|
|
5118
5422
|
"""Show the calculate all parameter dialog."""
|
|
5119
5423
|
dialog = CalcAllDialog(self)
|
|
5120
|
-
dialog.
|
|
5424
|
+
dialog.show()
|
|
5121
5425
|
|
|
5122
|
-
def show_calc_prox_dialog(self):
|
|
5426
|
+
def show_calc_prox_dialog(self, tutorial_example = False):
|
|
5123
5427
|
"""Show the proximity calc dialog"""
|
|
5124
|
-
dialog = ProxDialog(self)
|
|
5125
|
-
|
|
5428
|
+
dialog = ProxDialog(self, tutorial_example = True)
|
|
5429
|
+
if tutorial_example:
|
|
5430
|
+
dialog.show()
|
|
5431
|
+
else:
|
|
5432
|
+
dialog.exec()
|
|
5126
5433
|
|
|
5127
5434
|
def table_load_attrs(self):
|
|
5128
5435
|
|
|
@@ -5185,8 +5492,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5185
5492
|
self.load_channel(1, my_network.nodes, data = True)
|
|
5186
5493
|
self.delete_channel(0, False)
|
|
5187
5494
|
|
|
5188
|
-
my_network.id_overlay = my_network.edges.copy()
|
|
5189
|
-
|
|
5190
5495
|
self.show_gennodes_dialog()
|
|
5191
5496
|
|
|
5192
5497
|
my_network.edges = (my_network.nodes == 0) * my_network.edges
|
|
@@ -5195,7 +5500,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5195
5500
|
|
|
5196
5501
|
self.load_channel(1, my_network.edges, data = True)
|
|
5197
5502
|
self.load_channel(0, my_network.nodes, data = True)
|
|
5198
|
-
self.load_channel(3, my_network.id_overlay, data = True)
|
|
5199
5503
|
|
|
5200
5504
|
self.table_load_attrs()
|
|
5201
5505
|
|
|
@@ -5225,6 +5529,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5225
5529
|
|
|
5226
5530
|
self.load_channel(0, my_network.edges, data = True)
|
|
5227
5531
|
|
|
5532
|
+
try:
|
|
5533
|
+
self.branch_dict[0] = self.branch_dict[1]
|
|
5534
|
+
self.branch_dict[1] = None
|
|
5535
|
+
except:
|
|
5536
|
+
pass
|
|
5537
|
+
|
|
5228
5538
|
self.delete_channel(1, False)
|
|
5229
5539
|
|
|
5230
5540
|
my_network.morph_proximity(search = [3,3], fastdil = True)
|
|
@@ -5241,14 +5551,51 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5241
5551
|
dialog = CentroidDialog(self)
|
|
5242
5552
|
dialog.exec()
|
|
5243
5553
|
|
|
5244
|
-
def
|
|
5554
|
+
def handle_identity_net_calc(self):
|
|
5555
|
+
|
|
5556
|
+
try:
|
|
5557
|
+
|
|
5558
|
+
def confirm_dialog():
|
|
5559
|
+
"""Shows a dialog asking user to confirm and input connection limit"""
|
|
5560
|
+
from PyQt6.QtWidgets import QInputDialog
|
|
5561
|
+
|
|
5562
|
+
value, ok = QInputDialog.getInt(
|
|
5563
|
+
None, # parent widget
|
|
5564
|
+
"Confirm", # window title
|
|
5565
|
+
"Calculate Identity Network\n\n"
|
|
5566
|
+
"Connect nodes that share an identity - useful for nodes that\n"
|
|
5567
|
+
"overlap in identity to some degree.\n\n"
|
|
5568
|
+
"Enter maximum connections per node within same identity:",
|
|
5569
|
+
5, # default value
|
|
5570
|
+
1, # minimum value
|
|
5571
|
+
1000, # maximum value
|
|
5572
|
+
1 # step
|
|
5573
|
+
)
|
|
5574
|
+
|
|
5575
|
+
if ok:
|
|
5576
|
+
return True, value
|
|
5577
|
+
else:
|
|
5578
|
+
return False, None
|
|
5579
|
+
|
|
5580
|
+
confirm, val = confirm_dialog()
|
|
5581
|
+
|
|
5582
|
+
if confirm:
|
|
5583
|
+
my_network.create_id_network(val)
|
|
5584
|
+
self.table_load_attrs()
|
|
5585
|
+
else:
|
|
5586
|
+
return
|
|
5587
|
+
|
|
5588
|
+
except:
|
|
5589
|
+
pass
|
|
5590
|
+
|
|
5591
|
+
def show_dilate_dialog(self, args = None):
|
|
5245
5592
|
"""show the dilate dialog"""
|
|
5246
|
-
dialog = DilateDialog(self)
|
|
5247
|
-
dialog.
|
|
5593
|
+
dialog = DilateDialog(self, args)
|
|
5594
|
+
dialog.show()
|
|
5248
5595
|
|
|
5249
|
-
def show_erode_dialog(self):
|
|
5596
|
+
def show_erode_dialog(self, args = None):
|
|
5250
5597
|
"""show the erode dialog"""
|
|
5251
|
-
dialog = ErodeDialog(self)
|
|
5598
|
+
dialog = ErodeDialog(self, args)
|
|
5252
5599
|
dialog.exec()
|
|
5253
5600
|
|
|
5254
5601
|
def show_hole_dialog(self):
|
|
@@ -5256,6 +5603,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5256
5603
|
dialog = HoleDialog(self)
|
|
5257
5604
|
dialog.exec()
|
|
5258
5605
|
|
|
5606
|
+
def show_filament_dialog(self):
|
|
5607
|
+
"""show the filament dialog"""
|
|
5608
|
+
dialog = FilamentDialog(self)
|
|
5609
|
+
dialog.show()
|
|
5610
|
+
|
|
5259
5611
|
def show_label_dialog(self):
|
|
5260
5612
|
"""Show the label dialog"""
|
|
5261
5613
|
dialog = LabelDialog(self)
|
|
@@ -5266,13 +5618,20 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5266
5618
|
dialog = SLabelDialog(self)
|
|
5267
5619
|
dialog.exec()
|
|
5268
5620
|
|
|
5269
|
-
def show_thresh_dialog(self):
|
|
5621
|
+
def show_thresh_dialog(self, tutorial_example = False):
|
|
5270
5622
|
"""Show threshold dialog"""
|
|
5271
5623
|
if self.machine_window is not None:
|
|
5272
5624
|
return
|
|
5273
5625
|
|
|
5274
5626
|
dialog = ThresholdDialog(self)
|
|
5275
|
-
|
|
5627
|
+
if not tutorial_example:
|
|
5628
|
+
dialog.exec()
|
|
5629
|
+
else:
|
|
5630
|
+
dialog.show()
|
|
5631
|
+
|
|
5632
|
+
def show_machine_window_tutorial(self):
|
|
5633
|
+
dialog = MachineWindow(self, tutorial_example = True)
|
|
5634
|
+
dialog.show()
|
|
5276
5635
|
|
|
5277
5636
|
|
|
5278
5637
|
def show_mask_dialog(self):
|
|
@@ -5309,15 +5668,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5309
5668
|
dialog.exec()
|
|
5310
5669
|
|
|
5311
5670
|
|
|
5312
|
-
def show_gennodes_dialog(self, down_factor = None, called = False):
|
|
5671
|
+
def show_gennodes_dialog(self, down_factor = None, called = False, tutorial_example = False):
|
|
5313
5672
|
"""show the gennodes dialog"""
|
|
5314
5673
|
gennodes = GenNodesDialog(self, down_factor = down_factor, called = called)
|
|
5315
|
-
|
|
5674
|
+
if not tutorial_example:
|
|
5675
|
+
gennodes.exec()
|
|
5676
|
+
else:
|
|
5677
|
+
gennodes.show()
|
|
5316
5678
|
|
|
5317
|
-
def show_branch_dialog(self, called = False):
|
|
5679
|
+
def show_branch_dialog(self, called = False, tutorial_example = False):
|
|
5318
5680
|
"""Show the branch label dialog"""
|
|
5319
|
-
dialog = BranchDialog(self, called = called)
|
|
5320
|
-
|
|
5681
|
+
dialog = BranchDialog(self, called = called, tutorial_example = tutorial_example)
|
|
5682
|
+
if tutorial_example:
|
|
5683
|
+
dialog.show()
|
|
5684
|
+
else:
|
|
5685
|
+
dialog.exec()
|
|
5321
5686
|
|
|
5322
5687
|
def voronoi(self):
|
|
5323
5688
|
|
|
@@ -5333,7 +5698,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5333
5698
|
def show_modify_dialog(self):
|
|
5334
5699
|
"""Show the network modify dialog"""
|
|
5335
5700
|
dialog = ModifyDialog(self)
|
|
5336
|
-
dialog.
|
|
5701
|
+
dialog.show()
|
|
5337
5702
|
|
|
5338
5703
|
|
|
5339
5704
|
def show_binarize_dialog(self):
|
|
@@ -5347,11 +5712,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5347
5712
|
dialog = ResizeDialog(self)
|
|
5348
5713
|
dialog.exec()
|
|
5349
5714
|
|
|
5715
|
+
def show_clean_dialog(self):
|
|
5716
|
+
dialog = CleanDialog(self)
|
|
5717
|
+
dialog.show()
|
|
5350
5718
|
|
|
5351
5719
|
def show_properties_dialog(self):
|
|
5352
5720
|
"""Show the properties dialog"""
|
|
5353
5721
|
dialog = PropertiesDialog(self)
|
|
5354
|
-
dialog.
|
|
5722
|
+
dialog.show()
|
|
5355
5723
|
|
|
5356
5724
|
def show_brightness_dialog(self):
|
|
5357
5725
|
"""Show the brightness/contrast control dialog."""
|
|
@@ -5963,7 +6331,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5963
6331
|
msg = QMessageBox()
|
|
5964
6332
|
msg.setIcon(QMessageBox.Icon.Question)
|
|
5965
6333
|
msg.setText("Image Format Alert")
|
|
5966
|
-
msg.setInformativeText(f"This image is a different shape than the ones loaded into the viewer window.
|
|
6334
|
+
msg.setInformativeText(f"This image is a different shape than the ones loaded into the viewer window. This program is not designed to accomodate loading of differently sized images.\nPress yes to resize the new image to the other images. Press no to go back.")
|
|
5967
6335
|
msg.setWindowTitle("Resize")
|
|
5968
6336
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
5969
6337
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
@@ -5990,11 +6358,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5990
6358
|
if 'YResolution' in tags:
|
|
5991
6359
|
y_res = tags['YResolution'].value
|
|
5992
6360
|
y_scale = y_res[1] / y_res[0] if isinstance(y_res, tuple) else 1.0 / y_res
|
|
5993
|
-
|
|
5994
|
-
if x_scale
|
|
6361
|
+
|
|
6362
|
+
if x_scale == None:
|
|
5995
6363
|
x_scale = 1
|
|
5996
|
-
if z_scale
|
|
6364
|
+
if z_scale == None:
|
|
5997
6365
|
z_scale = 1
|
|
6366
|
+
if x_scale == 1 and z_scale == 1:
|
|
6367
|
+
return
|
|
5998
6368
|
|
|
5999
6369
|
return x_scale, z_scale
|
|
6000
6370
|
|
|
@@ -6029,7 +6399,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6029
6399
|
print(f"xy_scale property set to {my_network.xy_scale}; z_scale property set to {my_network.z_scale}")
|
|
6030
6400
|
except:
|
|
6031
6401
|
pass
|
|
6032
|
-
|
|
6402
|
+
test_channel_data = tifffile.imread(filename)
|
|
6403
|
+
if len(test_channel_data.shape) not in (2, 3, 4):
|
|
6404
|
+
print("Invalid Shape")
|
|
6405
|
+
return
|
|
6406
|
+
self.channel_data[channel_index] = test_channel_data
|
|
6033
6407
|
|
|
6034
6408
|
elif file_extension == 'nii':
|
|
6035
6409
|
import nibabel as nib
|
|
@@ -6107,7 +6481,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6107
6481
|
if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
|
|
6108
6482
|
try:
|
|
6109
6483
|
if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
|
|
6110
|
-
self.resizing = True
|
|
6111
6484
|
reset_resize = True
|
|
6112
6485
|
self.highlight_overlay = None
|
|
6113
6486
|
except:
|
|
@@ -6122,6 +6495,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6122
6495
|
if self.confirm_resize_dialog():
|
|
6123
6496
|
self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
|
|
6124
6497
|
break
|
|
6498
|
+
else:
|
|
6499
|
+
return
|
|
6125
6500
|
|
|
6126
6501
|
if not begin_paint:
|
|
6127
6502
|
if channel_index == 0:
|
|
@@ -6194,7 +6569,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6194
6569
|
|
|
6195
6570
|
if self.shape == self.channel_data[channel_index].shape:
|
|
6196
6571
|
preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
|
|
6197
|
-
|
|
6572
|
+
self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
|
|
6573
|
+
else:
|
|
6574
|
+
if self.shape is not None:
|
|
6575
|
+
self.resizing = True
|
|
6576
|
+
self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
|
|
6577
|
+
ylim, xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
6578
|
+
preserve_zoom = (xlim, ylim)
|
|
6198
6579
|
if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
|
|
6199
6580
|
self.throttle = True
|
|
6200
6581
|
else:
|
|
@@ -6202,7 +6583,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6202
6583
|
|
|
6203
6584
|
|
|
6204
6585
|
self.img_height, self.img_width = self.shape[1], self.shape[2]
|
|
6205
|
-
self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
6206
6586
|
|
|
6207
6587
|
self.completed_paint_strokes = [] #Reset pending paint operations
|
|
6208
6588
|
self.current_stroke_points = []
|
|
@@ -6413,6 +6793,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6413
6793
|
#print(f"Saved {self.channel_names[ch_index]}" + (f" to: {filename}" if filename else "")) # Debug print
|
|
6414
6794
|
|
|
6415
6795
|
except Exception as e:
|
|
6796
|
+
import traceback
|
|
6797
|
+
traceback.print_exc()
|
|
6416
6798
|
QMessageBox.critical(
|
|
6417
6799
|
self,
|
|
6418
6800
|
"Error Saving File",
|
|
@@ -6436,12 +6818,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6436
6818
|
def update_slice(self):
|
|
6437
6819
|
"""Queue a slice update when slider moves."""
|
|
6438
6820
|
# Store current view settings
|
|
6439
|
-
if
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
else:
|
|
6443
|
-
current_xlim = None
|
|
6444
|
-
current_ylim = None
|
|
6821
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
6822
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
6823
|
+
|
|
6445
6824
|
|
|
6446
6825
|
# Store the pending slice and view settings
|
|
6447
6826
|
self.pending_slice = (self.slice_slider.value(), (current_xlim, current_ylim))
|
|
@@ -6469,6 +6848,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6469
6848
|
self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6470
6849
|
elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
|
|
6471
6850
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
6851
|
+
|
|
6852
|
+
if self.resizing:
|
|
6853
|
+
self.highlight_overlay = None
|
|
6854
|
+
view_settings = ((-0.5, self.shape[2] - 0.5), (self.shape[1] - 0.5, -0.5))
|
|
6855
|
+
self.resizing = False
|
|
6472
6856
|
self.update_display(preserve_zoom=view_settings)
|
|
6473
6857
|
if self.pan_mode:
|
|
6474
6858
|
self.pan_button.click()
|
|
@@ -6826,6 +7210,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6826
7210
|
if current_xlim is not None and current_ylim is not None:
|
|
6827
7211
|
self.ax.set_xlim(current_xlim)
|
|
6828
7212
|
self.ax.set_ylim(current_ylim)
|
|
7213
|
+
|
|
7214
|
+
if hasattr(self, 'scalebar_artists') and self.scalebar_artists:
|
|
7215
|
+
self._draw_scalebar()
|
|
7216
|
+
else:
|
|
7217
|
+
self._remove_scalebar()
|
|
7218
|
+
|
|
6829
7219
|
|
|
6830
7220
|
if reset_resize:
|
|
6831
7221
|
self.resizing = False
|
|
@@ -6836,8 +7226,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6836
7226
|
|
|
6837
7227
|
except Exception as e:
|
|
6838
7228
|
pass
|
|
6839
|
-
#import traceback
|
|
6840
|
-
#print(traceback.format_exc())
|
|
6841
7229
|
|
|
6842
7230
|
|
|
6843
7231
|
def get_channel_image(self, channel):
|
|
@@ -6947,12 +7335,73 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6947
7335
|
dialog = RadDialog(self)
|
|
6948
7336
|
dialog.exec()
|
|
6949
7337
|
|
|
7338
|
+
def handle_sa(self):
|
|
7339
|
+
|
|
7340
|
+
try:
|
|
7341
|
+
|
|
7342
|
+
if self.shape[0] == 1:
|
|
7343
|
+
print("The image is 2D and therefore does not have surface areas")
|
|
7344
|
+
return
|
|
7345
|
+
|
|
7346
|
+
surface_areas = n3d.get_surface_areas(self.channel_data[self.active_channel], xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
7347
|
+
|
|
7348
|
+
if self.active_channel == 0:
|
|
7349
|
+
self.surface_area_dict[0] = surface_areas
|
|
7350
|
+
elif self.active_channel == 1:
|
|
7351
|
+
self.surface_area_dict[1] = surface_areas
|
|
7352
|
+
elif self.active_channel == 2:
|
|
7353
|
+
self.surface_area_dict[2] = surface_areas
|
|
7354
|
+
elif self.active_channel == 3:
|
|
7355
|
+
self.surface_area_dict[3] = surface_areas
|
|
7356
|
+
|
|
7357
|
+
self.format_for_upperright_table(surface_areas, title = '~Surface Areas of Objects (Jagged Faces)', metric='ObjectID', value='~Surface Area (Scaled)')
|
|
7358
|
+
|
|
7359
|
+
except Exception as e:
|
|
7360
|
+
print(f"Error: {e}")
|
|
7361
|
+
|
|
7362
|
+
def handle_sphericity(self):
|
|
7363
|
+
|
|
7364
|
+
try:
|
|
7365
|
+
|
|
7366
|
+
if self.shape[0] == 1:
|
|
7367
|
+
print("The image is 2D and therefore does not have sphericities")
|
|
7368
|
+
return
|
|
7369
|
+
|
|
7370
|
+
self.volumes()
|
|
7371
|
+
self.handle_sa()
|
|
7372
|
+
volumes = self.volume_dict[self.active_channel]
|
|
7373
|
+
surface_areas = self.surface_area_dict[self.active_channel]
|
|
7374
|
+
|
|
7375
|
+
sphericities = {
|
|
7376
|
+
label: (np.pi**(1/3) * (6 * volumes[label])**(2/3)) / surface_areas[label]
|
|
7377
|
+
for label in volumes.keys()
|
|
7378
|
+
if label in surface_areas and volumes[label] > 0 and surface_areas[label] > 0
|
|
7379
|
+
}
|
|
7380
|
+
|
|
7381
|
+
if self.active_channel == 0:
|
|
7382
|
+
self.sphericity_dict[0] = sphericities
|
|
7383
|
+
elif self.active_channel == 1:
|
|
7384
|
+
self.sphericity_dict[1] = sphericities
|
|
7385
|
+
elif self.active_channel == 2:
|
|
7386
|
+
self.sphericity_dict[2] = sphericities
|
|
7387
|
+
elif self.active_channel == 3:
|
|
7388
|
+
self.sphericity_dict[3] = sphericities
|
|
7389
|
+
|
|
7390
|
+
self.format_for_upperright_table(sphericities, title = 'Sphericities of Objects', metric='ObjectID', value='Sphericity')
|
|
7391
|
+
|
|
7392
|
+
except Exception as e:
|
|
7393
|
+
print(f"Error: {e}")
|
|
7394
|
+
|
|
7395
|
+
def show_branchstat_dialog(self):
|
|
7396
|
+
dialog = BranchStatDialog(self)
|
|
7397
|
+
dialog.exec()
|
|
7398
|
+
|
|
6950
7399
|
def show_interaction_dialog(self):
|
|
6951
7400
|
dialog = InteractionDialog(self)
|
|
6952
7401
|
dialog.exec()
|
|
6953
7402
|
|
|
6954
|
-
def show_violin_dialog(self):
|
|
6955
|
-
dialog = ViolinDialog(self)
|
|
7403
|
+
def show_violin_dialog(self, called = False):
|
|
7404
|
+
dialog = ViolinDialog(self, called = called)
|
|
6956
7405
|
dialog.show()
|
|
6957
7406
|
|
|
6958
7407
|
def show_degree_dialog(self):
|
|
@@ -7159,7 +7608,7 @@ class CustomTableView(QTableView):
|
|
|
7159
7608
|
else: # Bottom tables
|
|
7160
7609
|
# Add Find action
|
|
7161
7610
|
find_menu = context_menu.addMenu("Find")
|
|
7162
|
-
find_action = find_menu.addAction("Find Node/Edge")
|
|
7611
|
+
find_action = find_menu.addAction("Find Node/Edge/")
|
|
7163
7612
|
find_pair_action = find_menu.addAction("Find Pair")
|
|
7164
7613
|
find_action.triggered.connect(lambda: self.handle_find_action(
|
|
7165
7614
|
index.row(), index.column(),
|
|
@@ -7752,7 +8201,7 @@ class PropertiesDialog(QDialog):
|
|
|
7752
8201
|
def __init__(self, parent=None):
|
|
7753
8202
|
super().__init__(parent)
|
|
7754
8203
|
self.setWindowTitle("Properties")
|
|
7755
|
-
self.setModal(
|
|
8204
|
+
self.setModal(False)
|
|
7756
8205
|
|
|
7757
8206
|
layout = QFormLayout(self)
|
|
7758
8207
|
|
|
@@ -7805,9 +8254,9 @@ class PropertiesDialog(QDialog):
|
|
|
7805
8254
|
run_button.clicked.connect(self.run_properties)
|
|
7806
8255
|
layout.addWidget(run_button)
|
|
7807
8256
|
|
|
7808
|
-
report_button = QPushButton("Report Properties (Show in Top Right Tables)")
|
|
7809
|
-
report_button.clicked.connect(self.report)
|
|
7810
|
-
layout.addWidget(report_button)
|
|
8257
|
+
self.report_button = QPushButton("Report Properties (Show in Top Right Tables)")
|
|
8258
|
+
self.report_button.clicked.connect(self.report)
|
|
8259
|
+
layout.addWidget(self.report_button)
|
|
7811
8260
|
|
|
7812
8261
|
def check_checked(self, ques):
|
|
7813
8262
|
|
|
@@ -8288,11 +8737,6 @@ class MergeNodeIdDialog(QDialog):
|
|
|
8288
8737
|
self.mode_selector.addItems(["Auto-Binarize(Otsu)/Presegmented", "Manual (Interactive Thresholder)"])
|
|
8289
8738
|
self.mode_selector.setCurrentIndex(1) # Default to Mode 1
|
|
8290
8739
|
layout.addRow("Binarization Strategy:", self.mode_selector)
|
|
8291
|
-
|
|
8292
|
-
self.umap = QPushButton("If using manual threshold: Also generate identity UMAP?")
|
|
8293
|
-
self.umap.setCheckable(True)
|
|
8294
|
-
self.umap.setChecked(True)
|
|
8295
|
-
layout.addWidget(self.umap)
|
|
8296
8740
|
|
|
8297
8741
|
self.include = QPushButton("Include When a Node is Negative for an ID?")
|
|
8298
8742
|
self.include.setCheckable(True)
|
|
@@ -8349,7 +8793,7 @@ class MergeNodeIdDialog(QDialog):
|
|
|
8349
8793
|
z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
|
|
8350
8794
|
data = self.parent().channel_data[0]
|
|
8351
8795
|
include = self.include.isChecked()
|
|
8352
|
-
umap =
|
|
8796
|
+
umap = True
|
|
8353
8797
|
|
|
8354
8798
|
if data is None:
|
|
8355
8799
|
return
|
|
@@ -8484,18 +8928,23 @@ class MergeNodeIdDialog(QDialog):
|
|
|
8484
8928
|
all_keys = id_dicts[0].keys()
|
|
8485
8929
|
result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
|
|
8486
8930
|
|
|
8487
|
-
|
|
8488
|
-
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity (Save this Table for "Analyze -> Stats -> Show Violins")')
|
|
8489
|
-
if umap:
|
|
8490
|
-
my_network.identity_umap(result)
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
8931
|
QMessageBox.information(
|
|
8494
8932
|
self,
|
|
8495
8933
|
"Success",
|
|
8496
8934
|
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. If desired, please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. If desired, please save the outputted mean intensity table to use with 'Analyze -> Stats -> Show Violins'. (Press Help [above] for more info)"
|
|
8497
8935
|
)
|
|
8498
8936
|
|
|
8937
|
+
print("Please save your identity table if desired for use with the violin plot and intensity neighborhoods function")
|
|
8938
|
+
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity (Save this Table for "Analyze -> Stats -> Show Violins")', save = True)
|
|
8939
|
+
try:
|
|
8940
|
+
self.parent().show_violin_dialog(called = True)
|
|
8941
|
+
QMessageBox.information(
|
|
8942
|
+
self,
|
|
8943
|
+
"FYI",
|
|
8944
|
+
"Here is the violin plot/intensity neighborhoods function control window for the aforementioned table. Feel free to close these windows if you do not desire to use this analysis, however you will need to reference the saved table to get back here."
|
|
8945
|
+
)
|
|
8946
|
+
except:
|
|
8947
|
+
pass
|
|
8499
8948
|
self.accept()
|
|
8500
8949
|
else:
|
|
8501
8950
|
my_network.merge_node_ids(selected_path, data, include)
|
|
@@ -8614,7 +9063,7 @@ class Show3dDialog(QDialog):
|
|
|
8614
9063
|
self.cubic = QPushButton("Cubic")
|
|
8615
9064
|
self.cubic.setCheckable(True)
|
|
8616
9065
|
self.cubic.setChecked(False)
|
|
8617
|
-
layout.addRow("Use cubic downsample (Slower but preserves
|
|
9066
|
+
layout.addRow("Use cubic downsample (Slower but preserves visualization better potentially)?", self.cubic)
|
|
8618
9067
|
|
|
8619
9068
|
self.box = QPushButton("Box")
|
|
8620
9069
|
self.box.setCheckable(True)
|
|
@@ -8665,6 +9114,9 @@ class Show3dDialog(QDialog):
|
|
|
8665
9114
|
if visible:
|
|
8666
9115
|
arrays_4d.append(channel)
|
|
8667
9116
|
|
|
9117
|
+
if self.parent().thresh_window_ref is not None:
|
|
9118
|
+
self.parent().thresh_window_ref.make_full_highlight()
|
|
9119
|
+
|
|
8668
9120
|
if self.parent().highlight_overlay is not None or self.parent().mini_overlay_data is not None:
|
|
8669
9121
|
if self.parent().mini_overlay == True:
|
|
8670
9122
|
self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
@@ -8761,45 +9213,51 @@ class IdOverlayDialog(QDialog):
|
|
|
8761
9213
|
|
|
8762
9214
|
def idoverlay(self):
|
|
8763
9215
|
|
|
8764
|
-
accepted_mode = self.mode_selector.currentIndex()
|
|
8765
|
-
|
|
8766
9216
|
try:
|
|
8767
|
-
downsample = float(self.downsample.text()) if self.downsample.text() else None
|
|
8768
|
-
except ValueError:
|
|
8769
|
-
downsample = None
|
|
8770
9217
|
|
|
8771
|
-
|
|
9218
|
+
accepted_mode = self.mode_selector.currentIndex()
|
|
8772
9219
|
|
|
8773
|
-
|
|
9220
|
+
try:
|
|
9221
|
+
downsample = float(self.downsample.text()) if self.downsample.text() else None
|
|
9222
|
+
except ValueError:
|
|
9223
|
+
downsample = None
|
|
8774
9224
|
|
|
8775
|
-
|
|
9225
|
+
if accepted_mode == 0:
|
|
8776
9226
|
|
|
8777
|
-
|
|
8778
|
-
return
|
|
9227
|
+
if my_network.node_centroids is None:
|
|
8779
9228
|
|
|
8780
|
-
|
|
9229
|
+
self.parent().show_centroid_dialog()
|
|
8781
9230
|
|
|
8782
|
-
|
|
9231
|
+
if my_network.node_centroids is None:
|
|
9232
|
+
return
|
|
8783
9233
|
|
|
8784
|
-
|
|
9234
|
+
elif accepted_mode == 1:
|
|
8785
9235
|
|
|
8786
|
-
|
|
8787
|
-
return
|
|
9236
|
+
if my_network.edge_centroids is None:
|
|
8788
9237
|
|
|
8789
|
-
|
|
9238
|
+
self.parent().show_centroid_dialog()
|
|
8790
9239
|
|
|
8791
|
-
|
|
9240
|
+
if my_network.edge_centroids is None:
|
|
9241
|
+
return
|
|
8792
9242
|
|
|
8793
|
-
|
|
9243
|
+
if accepted_mode == 0:
|
|
8794
9244
|
|
|
8795
|
-
|
|
9245
|
+
my_network.id_overlay = my_network.draw_node_indices(down_factor = downsample)
|
|
8796
9246
|
|
|
8797
|
-
|
|
8798
|
-
my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
|
|
9247
|
+
elif accepted_mode == 1:
|
|
8799
9248
|
|
|
8800
|
-
|
|
9249
|
+
my_network.id_overlay = my_network.draw_edge_indices(down_factor = downsample)
|
|
9250
|
+
|
|
9251
|
+
if downsample is not None:
|
|
9252
|
+
my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
|
|
9253
|
+
|
|
9254
|
+
self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9255
|
+
|
|
9256
|
+
self.accept()
|
|
9257
|
+
|
|
9258
|
+
except:
|
|
9259
|
+
print(f"Error with Overlay Generation: {e}")
|
|
8801
9260
|
|
|
8802
|
-
self.accept()
|
|
8803
9261
|
|
|
8804
9262
|
class ColorOverlayDialog(QDialog):
|
|
8805
9263
|
|
|
@@ -8975,11 +9433,6 @@ class NetShowDialog(QDialog):
|
|
|
8975
9433
|
self.weighted.setCheckable(True)
|
|
8976
9434
|
self.weighted.setChecked(True)
|
|
8977
9435
|
layout.addRow("Use Weighted Network (Only for community graphs):", self.weighted)
|
|
8978
|
-
|
|
8979
|
-
# Optional saving:
|
|
8980
|
-
self.directory = QLineEdit()
|
|
8981
|
-
self.directory.setPlaceholderText("Does not save when empty")
|
|
8982
|
-
layout.addRow("Output Directory:", self.directory)
|
|
8983
9436
|
|
|
8984
9437
|
# Add Run button
|
|
8985
9438
|
run_button = QPushButton("Show Network")
|
|
@@ -8995,7 +9448,7 @@ class NetShowDialog(QDialog):
|
|
|
8995
9448
|
self.parent().show_centroid_dialog()
|
|
8996
9449
|
accepted_mode = self.mode_selector.currentIndex() # Convert to 1-based index
|
|
8997
9450
|
# Get directory (None if empty)
|
|
8998
|
-
directory =
|
|
9451
|
+
directory = None
|
|
8999
9452
|
|
|
9000
9453
|
weighted = self.weighted.isChecked()
|
|
9001
9454
|
|
|
@@ -9038,7 +9491,7 @@ class PartitionDialog(QDialog):
|
|
|
9038
9491
|
|
|
9039
9492
|
# Add mode selection dropdown
|
|
9040
9493
|
self.mode_selector = QComboBox()
|
|
9041
|
-
self.mode_selector.addItems(["
|
|
9494
|
+
self.mode_selector.addItems(["Louvain", "Label Propogation"])
|
|
9042
9495
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
9043
9496
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
9044
9497
|
|
|
@@ -9061,6 +9514,10 @@ class PartitionDialog(QDialog):
|
|
|
9061
9514
|
self.parent().prev_coms = None
|
|
9062
9515
|
|
|
9063
9516
|
accepted_mode = self.mode_selector.currentIndex()
|
|
9517
|
+
if accepted_mode == 0: #I switched where these are in the selection box
|
|
9518
|
+
accepted_mode = 1
|
|
9519
|
+
elif accepted_mode == 1:
|
|
9520
|
+
accepted_mode = 0
|
|
9064
9521
|
weighted = self.weighted.isChecked()
|
|
9065
9522
|
dostats = self.stats.isChecked()
|
|
9066
9523
|
|
|
@@ -9337,9 +9794,6 @@ class RadialDialog(QDialog):
|
|
|
9337
9794
|
self.distance = QLineEdit("50")
|
|
9338
9795
|
layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
|
|
9339
9796
|
|
|
9340
|
-
self.directory = QLineEdit("")
|
|
9341
|
-
layout.addRow("Output Directory:", self.directory)
|
|
9342
|
-
|
|
9343
9797
|
# Add Run button
|
|
9344
9798
|
run_button = QPushButton("Get Radial Distribution")
|
|
9345
9799
|
run_button.clicked.connect(self.radial)
|
|
@@ -9351,7 +9805,7 @@ class RadialDialog(QDialog):
|
|
|
9351
9805
|
|
|
9352
9806
|
distance = float(self.distance.text()) if self.distance.text().strip() else 50
|
|
9353
9807
|
|
|
9354
|
-
directory =
|
|
9808
|
+
directory = None
|
|
9355
9809
|
|
|
9356
9810
|
if my_network.node_centroids is None:
|
|
9357
9811
|
self.parent().show_centroid_dialog()
|
|
@@ -9635,9 +10089,6 @@ class NeighborIdentityDialog(QDialog):
|
|
|
9635
10089
|
else:
|
|
9636
10090
|
self.root = None
|
|
9637
10091
|
|
|
9638
|
-
self.directory = QLineEdit("")
|
|
9639
|
-
layout.addRow("Output Directory:", self.directory)
|
|
9640
|
-
|
|
9641
10092
|
self.mode = QComboBox()
|
|
9642
10093
|
self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
|
|
9643
10094
|
self.mode.setCurrentIndex(0)
|
|
@@ -9649,7 +10100,7 @@ class NeighborIdentityDialog(QDialog):
|
|
|
9649
10100
|
self.fastdil = QPushButton("Fast Dilate")
|
|
9650
10101
|
self.fastdil.setCheckable(True)
|
|
9651
10102
|
self.fastdil.setChecked(False)
|
|
9652
|
-
layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
|
|
10103
|
+
#layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
|
|
9653
10104
|
|
|
9654
10105
|
# Add Run button
|
|
9655
10106
|
run_button = QPushButton("Get Neighborhood Identity Distribution")
|
|
@@ -9665,7 +10116,7 @@ class NeighborIdentityDialog(QDialog):
|
|
|
9665
10116
|
except:
|
|
9666
10117
|
pass
|
|
9667
10118
|
|
|
9668
|
-
directory =
|
|
10119
|
+
directory = None
|
|
9669
10120
|
|
|
9670
10121
|
mode = self.mode.currentIndex()
|
|
9671
10122
|
|
|
@@ -10060,7 +10511,7 @@ class RadDialog(QDialog):
|
|
|
10060
10511
|
self.GPU = QPushButton("GPU")
|
|
10061
10512
|
self.GPU.setCheckable(True)
|
|
10062
10513
|
self.GPU.setChecked(False)
|
|
10063
|
-
layout.addRow("Use GPU:", self.GPU)
|
|
10514
|
+
#layout.addRow("Use GPU:", self.GPU)
|
|
10064
10515
|
|
|
10065
10516
|
|
|
10066
10517
|
# Add Run button
|
|
@@ -10094,9 +10545,6 @@ class RadDialog(QDialog):
|
|
|
10094
10545
|
print(f"Error: {e}")
|
|
10095
10546
|
|
|
10096
10547
|
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
10548
|
class InteractionDialog(QDialog):
|
|
10101
10549
|
|
|
10102
10550
|
def __init__(self, parent=None):
|
|
@@ -10138,7 +10586,7 @@ class InteractionDialog(QDialog):
|
|
|
10138
10586
|
self.fastdil = QPushButton("Fast Dilate")
|
|
10139
10587
|
self.fastdil.setCheckable(True)
|
|
10140
10588
|
self.fastdil.setChecked(False)
|
|
10141
|
-
layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
|
|
10589
|
+
#layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
|
|
10142
10590
|
|
|
10143
10591
|
# Add Run button
|
|
10144
10592
|
run_button = QPushButton("Calculate")
|
|
@@ -10181,34 +10629,40 @@ class InteractionDialog(QDialog):
|
|
|
10181
10629
|
|
|
10182
10630
|
class ViolinDialog(QDialog):
|
|
10183
10631
|
|
|
10184
|
-
def __init__(self, parent=None):
|
|
10185
|
-
|
|
10632
|
+
def __init__(self, parent=None, called = False):
|
|
10186
10633
|
super().__init__(parent)
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10634
|
+
if not called:
|
|
10635
|
+
QMessageBox.critical(
|
|
10636
|
+
self,
|
|
10637
|
+
"Notice",
|
|
10638
|
+
"Please select spreadsheet (Should be table output of 'File -> Images -> Node Identities -> Assign Node Identities from Overlap with Other Images'. Make sure to save that table as .csv/.xlsx and then load it here to use this.)"
|
|
10639
|
+
)
|
|
10194
10640
|
try:
|
|
10641
|
+
if not called:
|
|
10642
|
+
try:
|
|
10643
|
+
self.df = self.parent().load_file()
|
|
10644
|
+
except:
|
|
10645
|
+
return
|
|
10646
|
+
else:
|
|
10647
|
+
try:
|
|
10648
|
+
self.df = list(self.parent().tabbed_data.tables.values())[-1].model()._data
|
|
10649
|
+
except:
|
|
10650
|
+
pass
|
|
10651
|
+
try:
|
|
10652
|
+
self.backup_df = copy.deepcopy(self.df)
|
|
10653
|
+
except:
|
|
10654
|
+
pass
|
|
10195
10655
|
try:
|
|
10196
|
-
|
|
10656
|
+
# Get all identity lists and normalize the dataframe
|
|
10657
|
+
identity_lists = self.get_all_identity_lists()
|
|
10658
|
+
self.df = self.normalize_df_with_identity_centerpoints(self.df, identity_lists)
|
|
10197
10659
|
except:
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
self.backup_df = copy.deepcopy(self.df)
|
|
10201
|
-
# Get all identity lists and normalize the dataframe
|
|
10202
|
-
identity_lists = self.get_all_identity_lists()
|
|
10203
|
-
self.df = self.normalize_df_with_identity_centerpoints(self.df, identity_lists)
|
|
10204
|
-
|
|
10205
|
-
self.setWindowTitle("Violin Parameters")
|
|
10660
|
+
pass
|
|
10661
|
+
self.setWindowTitle("Violin/Neighborhood Parameters")
|
|
10206
10662
|
self.setModal(False)
|
|
10207
|
-
|
|
10208
10663
|
layout = QFormLayout(self)
|
|
10209
|
-
|
|
10664
|
+
|
|
10210
10665
|
if my_network.node_identities is not None:
|
|
10211
|
-
|
|
10212
10666
|
self.idens = QComboBox()
|
|
10213
10667
|
all_idens = list(set(my_network.node_identities.values()))
|
|
10214
10668
|
idens = []
|
|
@@ -10230,16 +10684,49 @@ class ViolinDialog(QDialog):
|
|
|
10230
10684
|
self.coms.addItems(coms)
|
|
10231
10685
|
self.coms.setCurrentIndex(0)
|
|
10232
10686
|
layout.addRow("Return Neighborhood/Community Violin Plots?", self.coms)
|
|
10233
|
-
|
|
10687
|
+
|
|
10234
10688
|
# Add Run button
|
|
10235
10689
|
run_button = QPushButton("Show Z-score-like Violin")
|
|
10236
10690
|
run_button.clicked.connect(self.run)
|
|
10237
10691
|
layout.addWidget(run_button)
|
|
10238
|
-
|
|
10692
|
+
|
|
10239
10693
|
run_button2 = QPushButton("Show Z-score UMAP")
|
|
10240
10694
|
run_button2.clicked.connect(self.run2)
|
|
10241
|
-
|
|
10695
|
+
self.mode_selector = QComboBox()
|
|
10696
|
+
self.mode_selector.addItems(["Label UMAP By Identity", "Label UMAP By Neighborhood/Community"])
|
|
10697
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
10698
|
+
layout.addRow("Execution Mode:", self.mode_selector)
|
|
10699
|
+
layout.addRow(self.mode_selector, run_button2)
|
|
10700
|
+
|
|
10701
|
+
# Add separator to visually group the clustering options
|
|
10702
|
+
from PyQt6.QtWidgets import QFrame
|
|
10703
|
+
separator = QFrame()
|
|
10704
|
+
separator.setFrameShape(QFrame.Shape.HLine)
|
|
10705
|
+
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
|
10706
|
+
layout.addRow(separator)
|
|
10707
|
+
|
|
10708
|
+
# Clustering options section (visually grouped)
|
|
10709
|
+
clustering_label = QLabel("<b>Clustering Options:</b>")
|
|
10710
|
+
layout.addRow(clustering_label)
|
|
10711
|
+
|
|
10712
|
+
# KMeans clustering
|
|
10713
|
+
run_button3 = QPushButton("Assign Neighborhoods via KMeans Clustering")
|
|
10714
|
+
run_button3.clicked.connect(self.run3)
|
|
10715
|
+
self.kmeans_num_input = QLineEdit()
|
|
10716
|
+
self.kmeans_num_input.setPlaceholderText("Auto (num neighborhoods)")
|
|
10717
|
+
self.kmeans_num_input.setMaximumWidth(150)
|
|
10718
|
+
from PyQt6.QtGui import QIntValidator
|
|
10719
|
+
self.kmeans_num_input.setValidator(QIntValidator(1, 1000))
|
|
10720
|
+
layout.addRow(run_button3, self.kmeans_num_input)
|
|
10721
|
+
|
|
10722
|
+
# Reassign identities checkbox
|
|
10723
|
+
self.reassign_identities_checkbox = QCheckBox("Reassign Identities Based on Clustering Results?")
|
|
10724
|
+
self.reassign_identities_checkbox.setChecked(False)
|
|
10725
|
+
layout.addRow(self.reassign_identities_checkbox)
|
|
10726
|
+
|
|
10242
10727
|
except:
|
|
10728
|
+
import traceback
|
|
10729
|
+
print(traceback.format_exc())
|
|
10243
10730
|
QTimer.singleShot(0, self.close)
|
|
10244
10731
|
|
|
10245
10732
|
def get_all_identity_lists(self):
|
|
@@ -10281,6 +10768,63 @@ class ViolinDialog(QDialog):
|
|
|
10281
10768
|
|
|
10282
10769
|
return identity_lists
|
|
10283
10770
|
|
|
10771
|
+
def prepare_data_for_umap(self, df, node_identities=None):
|
|
10772
|
+
"""
|
|
10773
|
+
Prepare data for UMAP visualization by z-score normalizing columns.
|
|
10774
|
+
|
|
10775
|
+
Args:
|
|
10776
|
+
df: DataFrame with first column as NodeID, rest as marker intensities
|
|
10777
|
+
node_identities: Optional dict mapping node_id (int) -> identity (string).
|
|
10778
|
+
If provided, only nodes present as keys will be kept.
|
|
10779
|
+
|
|
10780
|
+
Returns:
|
|
10781
|
+
dict: {node_id: [normalized_marker_values]}
|
|
10782
|
+
"""
|
|
10783
|
+
from sklearn.preprocessing import StandardScaler
|
|
10784
|
+
import numpy as np
|
|
10785
|
+
|
|
10786
|
+
# Store marker names (column headers) before converting to numpy array
|
|
10787
|
+
marker_names = df.columns[1:].tolist() # All columns except first (NodeID)
|
|
10788
|
+
node_id_col_name = df.columns[0] # Store the first column name (e.g., "NodeID")
|
|
10789
|
+
|
|
10790
|
+
# Extract node IDs from first column
|
|
10791
|
+
node_ids = df.iloc[:, 0].values
|
|
10792
|
+
# Extract marker data (all columns except first)
|
|
10793
|
+
X = df.iloc[:, 1:].values
|
|
10794
|
+
|
|
10795
|
+
# Z-score normalization (column-wise)
|
|
10796
|
+
scaler = StandardScaler() # Ultimately decided to normalize with the entirety of the available data (even cells without identities) since those cells' low expression should represent something of a ground truth of background expression which is relevant for normalizing.
|
|
10797
|
+
X_normalized = scaler.fit_transform(X)
|
|
10798
|
+
|
|
10799
|
+
# Filter if node_identities is provided
|
|
10800
|
+
if my_network.node_identities is not None: # And then after norm we can remove irrelevant cells as we don't random uninvolved cells to be considered in the grouping algorithms (ie umap and kmeans)
|
|
10801
|
+
# Get the valid node IDs from node_identities keys
|
|
10802
|
+
valid_node_ids = set(my_network.node_identities.keys())
|
|
10803
|
+
|
|
10804
|
+
# Create mask for valid node IDs
|
|
10805
|
+
mask = pd.Series(node_ids).isin(valid_node_ids).values
|
|
10806
|
+
|
|
10807
|
+
# Filter both node_ids and X_normalized using the mask
|
|
10808
|
+
node_ids = node_ids[mask]
|
|
10809
|
+
X_normalized = X_normalized[mask]
|
|
10810
|
+
|
|
10811
|
+
# Optional: Check if any rows remain after filtering
|
|
10812
|
+
if len(node_ids) == 0:
|
|
10813
|
+
raise ValueError("No matching nodes found between df and node_identities")
|
|
10814
|
+
|
|
10815
|
+
# Reconstruct DataFrame with normalized values
|
|
10816
|
+
self.ref_df = pd.DataFrame(X_normalized, columns=marker_names)
|
|
10817
|
+
self.ref_df.insert(0, node_id_col_name, node_ids) # Add NodeID column back as first column
|
|
10818
|
+
|
|
10819
|
+
# Create dictionary mapping node_id -> normalized row
|
|
10820
|
+
result_dict = {
|
|
10821
|
+
int(node_ids[i]): X_normalized[i].tolist()
|
|
10822
|
+
for i in range(len(node_ids))
|
|
10823
|
+
}
|
|
10824
|
+
|
|
10825
|
+
return result_dict
|
|
10826
|
+
|
|
10827
|
+
|
|
10284
10828
|
def normalize_df_with_identity_centerpoints(self, df, identity_lists):
|
|
10285
10829
|
"""
|
|
10286
10830
|
Normalize the entire dataframe using identity-specific centerpoints.
|
|
@@ -10312,7 +10856,7 @@ class ViolinDialog(QDialog):
|
|
|
10312
10856
|
# Get nodes that exist in both the identity list and the dataframe
|
|
10313
10857
|
valid_nodes = [node for node in node_list if node in df_copy.index]
|
|
10314
10858
|
if valid_nodes and ((str(identity) == str(column)) or str(identity) == f'{str(column)}+'):
|
|
10315
|
-
# Get the
|
|
10859
|
+
# Get the min value for this identity in this column
|
|
10316
10860
|
identity_min = df_copy.loc[valid_nodes, column].min()
|
|
10317
10861
|
centerpoint = identity_min
|
|
10318
10862
|
break # Found the match, no need to continue
|
|
@@ -10368,7 +10912,7 @@ class ViolinDialog(QDialog):
|
|
|
10368
10912
|
for column in range(table.model().columnCount(None)):
|
|
10369
10913
|
table.resizeColumnToContents(column)
|
|
10370
10914
|
|
|
10371
|
-
def run(self):
|
|
10915
|
+
def run(self, com = None):
|
|
10372
10916
|
|
|
10373
10917
|
def df_to_dict_by_rows(df, row_indices, title):
|
|
10374
10918
|
"""
|
|
@@ -10407,6 +10951,23 @@ class ViolinDialog(QDialog):
|
|
|
10407
10951
|
|
|
10408
10952
|
from . import neighborhoods
|
|
10409
10953
|
|
|
10954
|
+
try:
|
|
10955
|
+
if com:
|
|
10956
|
+
|
|
10957
|
+
self.ref_df = self.df
|
|
10958
|
+
|
|
10959
|
+
com_dict = n3d.invert_dict(my_network.communities)
|
|
10960
|
+
|
|
10961
|
+
com_list = com_dict[int(com)]
|
|
10962
|
+
|
|
10963
|
+
violin_dict = df_to_dict_by_rows(self.df, com_list, f"Z-Score-like Channel Intensities of Community/Neighborhood {com}, {len(com_list)} Nodes")
|
|
10964
|
+
|
|
10965
|
+
neighborhoods.create_violin_plots(violin_dict, graph_title=f"Z-Score-like Channel Intensities of Community/Neighborhood {com}, {len(com_list)} Nodes")
|
|
10966
|
+
|
|
10967
|
+
return
|
|
10968
|
+
except:
|
|
10969
|
+
pass
|
|
10970
|
+
|
|
10410
10971
|
try:
|
|
10411
10972
|
|
|
10412
10973
|
if self.idens.currentIndex() != 0:
|
|
@@ -10446,31 +11007,166 @@ class ViolinDialog(QDialog):
|
|
|
10446
11007
|
except:
|
|
10447
11008
|
pass
|
|
10448
11009
|
|
|
10449
|
-
|
|
10450
11010
|
def run2(self):
|
|
10451
|
-
def df_to_dict(df):
|
|
10452
|
-
# Make a copy to avoid modifying the original dataframe
|
|
10453
|
-
df_copy = df.copy()
|
|
10454
|
-
|
|
10455
|
-
# Set the first column as the index (row headers)
|
|
10456
|
-
df_copy = df_copy.set_index(df_copy.columns[0])
|
|
10457
|
-
|
|
10458
|
-
# Convert all remaining columns to float type (batch conversion)
|
|
10459
|
-
df_copy = df_copy.astype(float)
|
|
10460
|
-
|
|
10461
|
-
# Create the result dictionary
|
|
10462
|
-
result_dict = {}
|
|
10463
|
-
for row_idx in df_copy.index:
|
|
10464
|
-
result_dict[row_idx] = df_copy.loc[row_idx].tolist()
|
|
10465
|
-
|
|
10466
|
-
return result_dict
|
|
10467
11011
|
|
|
10468
11012
|
try:
|
|
10469
|
-
umap_dict =
|
|
10470
|
-
|
|
11013
|
+
umap_dict = self.prepare_data_for_umap(self.backup_df)
|
|
11014
|
+
mode = self.mode_selector.currentIndex()
|
|
11015
|
+
my_network.identity_umap(umap_dict, mode)
|
|
11016
|
+
except:
|
|
11017
|
+
import traceback
|
|
11018
|
+
print(traceback.format_exc())
|
|
11019
|
+
pass
|
|
11020
|
+
|
|
11021
|
+
def run3(self):
|
|
11022
|
+
num_clusters_text = self.kmeans_num_input.text()
|
|
11023
|
+
|
|
11024
|
+
if num_clusters_text:
|
|
11025
|
+
num_clusters = int(num_clusters_text)
|
|
11026
|
+
# Use specified number of clusters
|
|
11027
|
+
print(f"Using {num_clusters} clusters")
|
|
11028
|
+
else:
|
|
11029
|
+
num_clusters = None # Auto-determine
|
|
11030
|
+
print("Auto-determining number of clusters")
|
|
11031
|
+
try:
|
|
11032
|
+
cluster_dict = self.prepare_data_for_umap(self.backup_df)
|
|
11033
|
+
my_network.group_nodes_by_intensity(cluster_dict, count = num_clusters)
|
|
11034
|
+
|
|
11035
|
+
try:
|
|
11036
|
+
# Check if user wants to reassign identities
|
|
11037
|
+
if self.reassign_identities_checkbox.isChecked():
|
|
11038
|
+
# Invert the dict to get {neighborhood_id: [node_ids]}
|
|
11039
|
+
inverted_dict = n3d.invert_dict(my_network.communities)
|
|
11040
|
+
|
|
11041
|
+
# Dictionary to store old -> new neighborhood names
|
|
11042
|
+
neighborhood_rename_dict = {}
|
|
11043
|
+
neighborhood_items = list(inverted_dict.items())
|
|
11044
|
+
|
|
11045
|
+
def show_next_dialog(index=0):
|
|
11046
|
+
if index >= len(neighborhood_items):
|
|
11047
|
+
temp_dict = copy.deepcopy(neighborhood_rename_dict)
|
|
11048
|
+
for item in temp_dict:
|
|
11049
|
+
if temp_dict[item] == None:
|
|
11050
|
+
del neighborhood_rename_dict[item]
|
|
11051
|
+
# All dialogs done, apply the renaming
|
|
11052
|
+
for node_id, old_neighborhood_id in my_network.communities.items():
|
|
11053
|
+
try:
|
|
11054
|
+
# Only update identity if this neighborhood was renamed
|
|
11055
|
+
if old_neighborhood_id in neighborhood_rename_dict:
|
|
11056
|
+
my_network.node_identities[node_id] = neighborhood_rename_dict[old_neighborhood_id]
|
|
11057
|
+
# Otherwise, keep the existing identity (do nothing)
|
|
11058
|
+
except:
|
|
11059
|
+
pass
|
|
11060
|
+
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', title = 'Node Communities')
|
|
11061
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', title = 'Node Identities')
|
|
11062
|
+
self.accept()
|
|
11063
|
+
return
|
|
11064
|
+
|
|
11065
|
+
neighborhood_id, node_list = neighborhood_items[index]
|
|
11066
|
+
|
|
11067
|
+
plt.close()
|
|
11068
|
+
self.run(com = neighborhood_id)
|
|
11069
|
+
|
|
11070
|
+
# Filter self.ref_df to only nodes in this neighborhood
|
|
11071
|
+
mask = self.ref_df.iloc[:, 0].isin(node_list)
|
|
11072
|
+
filtered_df = self.ref_df[mask]
|
|
11073
|
+
|
|
11074
|
+
# Calculate average for each marker (skip first column which is NodeID)
|
|
11075
|
+
averages = filtered_df.iloc[:, 1:].mean()
|
|
11076
|
+
|
|
11077
|
+
# Show dialog to user
|
|
11078
|
+
dialog = NeighborhoodRenameDialog(
|
|
11079
|
+
neighborhood_id=neighborhood_id,
|
|
11080
|
+
averages=averages,
|
|
11081
|
+
node_count=len(node_list),
|
|
11082
|
+
parent=self
|
|
11083
|
+
)
|
|
11084
|
+
|
|
11085
|
+
def on_dialog_finished(result):
|
|
11086
|
+
if result == QDialog.DialogCode.Accepted:
|
|
11087
|
+
new_name = dialog.get_new_name()
|
|
11088
|
+
if new_name: # If user provided a non-empty name
|
|
11089
|
+
neighborhood_rename_dict[neighborhood_id] = new_name
|
|
11090
|
+
else: # User clicked OK but left it blank
|
|
11091
|
+
neighborhood_rename_dict[neighborhood_id] = None
|
|
11092
|
+
else:
|
|
11093
|
+
# User cancelled or closed window
|
|
11094
|
+
neighborhood_rename_dict[neighborhood_id] = None
|
|
11095
|
+
|
|
11096
|
+
# Show next dialog
|
|
11097
|
+
show_next_dialog(index + 1)
|
|
11098
|
+
|
|
11099
|
+
dialog.finished.connect(on_dialog_finished)
|
|
11100
|
+
dialog.show()
|
|
11101
|
+
|
|
11102
|
+
# Start the chain
|
|
11103
|
+
show_next_dialog(0)
|
|
11104
|
+
else:
|
|
11105
|
+
# No renaming needed, proceed directly
|
|
11106
|
+
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', title = 'Node Communities')
|
|
11107
|
+
self.accept()
|
|
11108
|
+
except:
|
|
11109
|
+
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', title = 'Node Communities')
|
|
11110
|
+
self.accept()
|
|
10471
11111
|
except:
|
|
11112
|
+
import traceback
|
|
11113
|
+
print(traceback.format_exc())
|
|
10472
11114
|
pass
|
|
10473
11115
|
|
|
11116
|
+
class NeighborhoodRenameDialog(QDialog):
|
|
11117
|
+
def __init__(self, neighborhood_id, averages, node_count, parent=None):
|
|
11118
|
+
super().__init__(parent)
|
|
11119
|
+
self.setWindowTitle(f"Rename Neighborhood {neighborhood_id}")
|
|
11120
|
+
self.setModal(False)
|
|
11121
|
+
|
|
11122
|
+
layout = QVBoxLayout(self)
|
|
11123
|
+
|
|
11124
|
+
# Instructions
|
|
11125
|
+
instructions = QLabel(
|
|
11126
|
+
f"<b>Neighborhood {neighborhood_id}</b><br>"
|
|
11127
|
+
f"Contains {node_count} nodes<br><br>"
|
|
11128
|
+
f"Please review the normalized average marker intensities below and provide a name for this neighborhood:"
|
|
11129
|
+
)
|
|
11130
|
+
instructions.setWordWrap(True)
|
|
11131
|
+
layout.addWidget(instructions)
|
|
11132
|
+
|
|
11133
|
+
# Create scrollable area for averages
|
|
11134
|
+
scroll = QScrollArea()
|
|
11135
|
+
scroll.setWidgetResizable(True)
|
|
11136
|
+
scroll.setMaximumHeight(300)
|
|
11137
|
+
|
|
11138
|
+
averages_widget = QWidget()
|
|
11139
|
+
averages_layout = QVBoxLayout(averages_widget)
|
|
11140
|
+
|
|
11141
|
+
# Display each marker average
|
|
11142
|
+
for marker_name, avg_value in averages.items():
|
|
11143
|
+
label = QLabel(f"{marker_name}: {avg_value:.4f}")
|
|
11144
|
+
averages_layout.addWidget(label)
|
|
11145
|
+
|
|
11146
|
+
scroll.setWidget(averages_widget)
|
|
11147
|
+
layout.addWidget(scroll)
|
|
11148
|
+
|
|
11149
|
+
# Text input for new name
|
|
11150
|
+
layout.addWidget(QLabel("<b>New Neighborhood Name:</b>"))
|
|
11151
|
+
self.name_input = QLineEdit()
|
|
11152
|
+
self.name_input.setPlaceholderText(f"Leave blank to not overwrite node identities for this neighborhood'")
|
|
11153
|
+
layout.addWidget(self.name_input)
|
|
11154
|
+
|
|
11155
|
+
# Buttons
|
|
11156
|
+
from PyQt6.QtWidgets import QDialogButtonBox
|
|
11157
|
+
button_box = QDialogButtonBox(
|
|
11158
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
11159
|
+
)
|
|
11160
|
+
button_box.accepted.connect(self.accept)
|
|
11161
|
+
button_box.rejected.connect(self.reject)
|
|
11162
|
+
layout.addWidget(button_box)
|
|
11163
|
+
|
|
11164
|
+
self.resize(400, 500)
|
|
11165
|
+
|
|
11166
|
+
def get_new_name(self):
|
|
11167
|
+
"""Return the new name entered by the user"""
|
|
11168
|
+
return self.name_input.text().strip()
|
|
11169
|
+
|
|
10474
11170
|
|
|
10475
11171
|
|
|
10476
11172
|
|
|
@@ -10827,7 +11523,7 @@ class ResizeDialog(QDialog):
|
|
|
10827
11523
|
|
|
10828
11524
|
|
|
10829
11525
|
# cubic checkbox (default False)
|
|
10830
|
-
self.cubic = QPushButton("Use Cubic Resize? (
|
|
11526
|
+
self.cubic = QPushButton("Use Cubic Resize? (For preserving visual characteristics, but not binary shape)")
|
|
10831
11527
|
self.cubic.setCheckable(True)
|
|
10832
11528
|
self.cubic.setChecked(False)
|
|
10833
11529
|
layout.addRow("Use cubic algorithm:", self.cubic)
|
|
@@ -10850,7 +11546,7 @@ class ResizeDialog(QDialog):
|
|
|
10850
11546
|
|
|
10851
11547
|
def run_resize(self, undo = False, upsize = True, special = False):
|
|
10852
11548
|
try:
|
|
10853
|
-
self.parent().resizing =
|
|
11549
|
+
self.parent().resizing = True
|
|
10854
11550
|
# Get parameters
|
|
10855
11551
|
try:
|
|
10856
11552
|
resize = float(self.resize.text()) if self.resize.text() else None
|
|
@@ -10965,6 +11661,7 @@ class ResizeDialog(QDialog):
|
|
|
10965
11661
|
if channel is not None:
|
|
10966
11662
|
self.parent().slice_slider.setMinimum(0)
|
|
10967
11663
|
self.parent().slice_slider.setMaximum(channel.shape[0] - 1)
|
|
11664
|
+
self.parent().shape = channel.shape
|
|
10968
11665
|
break
|
|
10969
11666
|
|
|
10970
11667
|
if not special:
|
|
@@ -11034,10 +11731,7 @@ class ResizeDialog(QDialog):
|
|
|
11034
11731
|
except Exception as e:
|
|
11035
11732
|
print(f"Error loading edge centroid table: {e}")
|
|
11036
11733
|
|
|
11037
|
-
|
|
11038
11734
|
self.parent().update_display()
|
|
11039
|
-
self.reset_fields()
|
|
11040
|
-
self.parent().resizing = False
|
|
11041
11735
|
self.accept()
|
|
11042
11736
|
|
|
11043
11737
|
except Exception as e:
|
|
@@ -11046,6 +11740,92 @@ class ResizeDialog(QDialog):
|
|
|
11046
11740
|
print(traceback.format_exc())
|
|
11047
11741
|
QMessageBox.critical(self, "Error", f"Failed to resize: {str(e)}")
|
|
11048
11742
|
|
|
11743
|
+
class CleanDialog(QDialog):
|
|
11744
|
+
def __init__(self, parent=None):
|
|
11745
|
+
super().__init__(parent)
|
|
11746
|
+
self.setWindowTitle("Some options for cleaning segmentation")
|
|
11747
|
+
self.setModal(False)
|
|
11748
|
+
|
|
11749
|
+
layout = QFormLayout(self)
|
|
11750
|
+
|
|
11751
|
+
# Add Run button
|
|
11752
|
+
run_button = QPushButton("Close")
|
|
11753
|
+
run_button.clicked.connect(self.close)
|
|
11754
|
+
layout.addRow("Close (Fill Small Gaps - Dilate then Erode by same amount):", run_button)
|
|
11755
|
+
|
|
11756
|
+
# Add Run button
|
|
11757
|
+
run_button = QPushButton("Open")
|
|
11758
|
+
run_button.clicked.connect(self.open)
|
|
11759
|
+
layout.addRow("Open (Eliminate Noise, Jagged Borders, and Small Connections Between Objects - Erode then Dilate by same amount):", run_button)
|
|
11760
|
+
|
|
11761
|
+
# Add Run button
|
|
11762
|
+
run_button = QPushButton("Fill Holes")
|
|
11763
|
+
run_button.clicked.connect(self.holes)
|
|
11764
|
+
layout.addRow("Call the fill holes function:", run_button)
|
|
11765
|
+
|
|
11766
|
+
# Add Run button
|
|
11767
|
+
run_button = QPushButton("Trace Filaments")
|
|
11768
|
+
run_button.clicked.connect(self.fils)
|
|
11769
|
+
layout.addRow("For Segmentations of Blood Vessels/Nerves: ", run_button)
|
|
11770
|
+
|
|
11771
|
+
# Add Run button
|
|
11772
|
+
run_button = QPushButton("Threshold Noise")
|
|
11773
|
+
run_button.clicked.connect(self.thresh)
|
|
11774
|
+
layout.addRow("Threshold Noise By Volume:", run_button)
|
|
11775
|
+
|
|
11776
|
+
def close(self):
|
|
11777
|
+
|
|
11778
|
+
try:
|
|
11779
|
+
self.parent().show_dilate_dialog(args = [1])
|
|
11780
|
+
self.parent().show_erode_dialog(args = [self.parent().last_dil])
|
|
11781
|
+
except:
|
|
11782
|
+
pass
|
|
11783
|
+
|
|
11784
|
+
def open(self):
|
|
11785
|
+
|
|
11786
|
+
try:
|
|
11787
|
+
self.parent().show_erode_dialog(args = [1])
|
|
11788
|
+
self.parent().show_dilate_dialog(args = [self.parent().last_ero])
|
|
11789
|
+
except:
|
|
11790
|
+
pass
|
|
11791
|
+
|
|
11792
|
+
def holes(self):
|
|
11793
|
+
|
|
11794
|
+
try:
|
|
11795
|
+
self.parent().show_hole_dialog()
|
|
11796
|
+
except:
|
|
11797
|
+
pass
|
|
11798
|
+
|
|
11799
|
+
def fils(self):
|
|
11800
|
+
|
|
11801
|
+
try:
|
|
11802
|
+
self.parent().show_filament_dialog()
|
|
11803
|
+
except:
|
|
11804
|
+
self.parent().show_filament_dialog()
|
|
11805
|
+
#pass
|
|
11806
|
+
|
|
11807
|
+
def thresh(self):
|
|
11808
|
+
try:
|
|
11809
|
+
if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
|
|
11810
|
+
self.parent().show_label_dialog()
|
|
11811
|
+
|
|
11812
|
+
if self.parent().volume_dict[self.parent().active_channel] is None:
|
|
11813
|
+
self.parent().volumes()
|
|
11814
|
+
|
|
11815
|
+
thresh_window = ThresholdWindow(self.parent(), 1)
|
|
11816
|
+
thresh_window.show() # Non-modal window
|
|
11817
|
+
self.parent().highlight_overlay = None
|
|
11818
|
+
#self.mini_overlay = False
|
|
11819
|
+
self.parent().mini_overlay_data = None
|
|
11820
|
+
except:
|
|
11821
|
+
import traceback
|
|
11822
|
+
print(traceback.format_exc())
|
|
11823
|
+
pass
|
|
11824
|
+
|
|
11825
|
+
|
|
11826
|
+
|
|
11827
|
+
|
|
11828
|
+
|
|
11049
11829
|
|
|
11050
11830
|
class OverrideDialog(QDialog):
|
|
11051
11831
|
def __init__(self, parent=None):
|
|
@@ -11200,11 +11980,7 @@ class BinarizeDialog(QDialog):
|
|
|
11200
11980
|
)
|
|
11201
11981
|
|
|
11202
11982
|
# Update both the display data and the network object
|
|
11203
|
-
self.parent().
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
# Update the corresponding property in my_network
|
|
11207
|
-
setattr(my_network, network_properties[self.parent().active_channel], result)
|
|
11983
|
+
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
11208
11984
|
|
|
11209
11985
|
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11210
11986
|
self.accept()
|
|
@@ -11252,11 +12028,7 @@ class LabelDialog(QDialog):
|
|
|
11252
12028
|
)
|
|
11253
12029
|
|
|
11254
12030
|
# Update both the display data and the network object
|
|
11255
|
-
self.parent().
|
|
11256
|
-
|
|
11257
|
-
|
|
11258
|
-
# Update the corresponding property in my_network
|
|
11259
|
-
setattr(my_network, network_properties[self.parent().active_channel], result)
|
|
12031
|
+
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
11260
12032
|
|
|
11261
12033
|
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11262
12034
|
self.accept()
|
|
@@ -11279,7 +12051,7 @@ class LabelDialog(QDialog):
|
|
|
11279
12051
|
class SLabelDialog(QDialog):
|
|
11280
12052
|
def __init__(self, parent=None):
|
|
11281
12053
|
super().__init__(parent)
|
|
11282
|
-
self.setWindowTitle("
|
|
12054
|
+
self.setWindowTitle("Label a binary image based on it's voxels proximity to labeled components of a second image?")
|
|
11283
12055
|
self.setModal(True)
|
|
11284
12056
|
|
|
11285
12057
|
layout = QFormLayout(self)
|
|
@@ -11291,7 +12063,7 @@ class SLabelDialog(QDialog):
|
|
|
11291
12063
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
11292
12064
|
layout.addRow("Prelabeled Array:", self.mode_selector)
|
|
11293
12065
|
|
|
11294
|
-
layout.addRow(QLabel("Will Label
|
|
12066
|
+
layout.addRow(QLabel("Will Label Binary Foreground Voxels in: "))
|
|
11295
12067
|
|
|
11296
12068
|
# Add mode selection dropdown
|
|
11297
12069
|
self.target_selector = QComboBox()
|
|
@@ -11303,10 +12075,20 @@ class SLabelDialog(QDialog):
|
|
|
11303
12075
|
self.GPU = QPushButton("GPU")
|
|
11304
12076
|
self.GPU.setCheckable(True)
|
|
11305
12077
|
self.GPU.setChecked(False)
|
|
11306
|
-
layout.addRow("Use GPU:", self.GPU)
|
|
12078
|
+
#layout.addRow("Use GPU:", self.GPU)
|
|
11307
12079
|
|
|
11308
12080
|
self.down_factor = QLineEdit("")
|
|
11309
|
-
layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
|
|
12081
|
+
#layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
|
|
12082
|
+
|
|
12083
|
+
self.label_mode = QComboBox()
|
|
12084
|
+
self.label_mode.addItems(["Label Individual Voxels based on Proximity", "Label Continuous Domains that Border Labels"])
|
|
12085
|
+
self.label_mode.setCurrentIndex(0)
|
|
12086
|
+
layout.addRow("Labeling Mode:", self.label_mode)
|
|
12087
|
+
|
|
12088
|
+
self.fix = QPushButton("Correct")
|
|
12089
|
+
self.fix.setCheckable(True)
|
|
12090
|
+
self.fix.setChecked(False)
|
|
12091
|
+
layout.addRow("Correct Nontouching Labels in post (Causes non-contiguous labels to merge with neighbors except the largest instance of that label):", self.fix)
|
|
11310
12092
|
|
|
11311
12093
|
# Add Run button
|
|
11312
12094
|
run_button = QPushButton("Run Smart Label")
|
|
@@ -11319,7 +12101,9 @@ class SLabelDialog(QDialog):
|
|
|
11319
12101
|
|
|
11320
12102
|
accepted_source = self.mode_selector.currentIndex()
|
|
11321
12103
|
accepted_target = self.target_selector.currentIndex()
|
|
12104
|
+
label_mode = self.label_mode.currentIndex()
|
|
11322
12105
|
GPU = self.GPU.isChecked()
|
|
12106
|
+
fix = self.fix.isChecked()
|
|
11323
12107
|
|
|
11324
12108
|
|
|
11325
12109
|
if accepted_source == accepted_target:
|
|
@@ -11332,27 +12116,51 @@ class SLabelDialog(QDialog):
|
|
|
11332
12116
|
down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
|
|
11333
12117
|
|
|
11334
12118
|
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
# Update both the display data and the network object
|
|
11338
|
-
binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
|
|
12119
|
+
if label_mode == 1:
|
|
11339
12120
|
|
|
11340
|
-
|
|
12121
|
+
label_mask = label_array == 0
|
|
11341
12122
|
|
|
11342
|
-
binary_array = binary_array * label_array
|
|
11343
12123
|
|
|
12124
|
+
#if self.parent().shape[0] != 1:
|
|
12125
|
+
# skele = n3d.skeletonize(binary_array)
|
|
12126
|
+
# skele = n3d.fill_holes_3d(skele)
|
|
12127
|
+
skele = n3d.skeletonize(binary_array)
|
|
12128
|
+
skele = label_mask * skele
|
|
12129
|
+
binary_array = label_mask * binary_array
|
|
12130
|
+
del label_mask
|
|
12131
|
+
skele, _ = n3d.label_objects(skele)
|
|
12132
|
+
skele = pxt.label_continuous(skele, label_array)
|
|
12133
|
+
skele = skele + label_array
|
|
12134
|
+
binary_array = sdl.smart_label(binary_array, skele, GPU = False, remove_template = False)
|
|
12135
|
+
binary_array = self.parent().separate_nontouching_objects(binary_array, max_val=np.max(binary_array), branches = True)
|
|
12136
|
+
#binary_array = binary_array + label_array
|
|
11344
12137
|
self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11345
|
-
|
|
11346
12138
|
self.accept()
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
12139
|
+
|
|
12140
|
+
else:
|
|
12141
|
+
|
|
12142
|
+
try:
|
|
12143
|
+
|
|
12144
|
+
# Update both the display data and the network object
|
|
12145
|
+
binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
|
|
12146
|
+
if fix:
|
|
12147
|
+
binary_array = self.parent().separate_nontouching_objects(binary_array, max_val=np.max(binary_array), branches = True)
|
|
12148
|
+
|
|
12149
|
+
self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12150
|
+
|
|
12151
|
+
self.accept()
|
|
12152
|
+
|
|
12153
|
+
except Exception as e:
|
|
12154
|
+
QMessageBox.critical(
|
|
12155
|
+
self,
|
|
12156
|
+
"Error",
|
|
12157
|
+
f"Error running smart label: {str(e)}"
|
|
12158
|
+
)
|
|
11354
12159
|
|
|
11355
12160
|
except Exception as e:
|
|
12161
|
+
import traceback
|
|
12162
|
+
traceback.print_exc()
|
|
12163
|
+
|
|
11356
12164
|
QMessageBox.critical(
|
|
11357
12165
|
self,
|
|
11358
12166
|
"Error",
|
|
@@ -11364,7 +12172,7 @@ class ThresholdDialog(QDialog):
|
|
|
11364
12172
|
def __init__(self, parent=None):
|
|
11365
12173
|
super().__init__(parent)
|
|
11366
12174
|
self.setWindowTitle("Choose Threshold Mode")
|
|
11367
|
-
self.setModal(
|
|
12175
|
+
self.setModal(False)
|
|
11368
12176
|
|
|
11369
12177
|
layout = QFormLayout(self)
|
|
11370
12178
|
|
|
@@ -11553,33 +12361,40 @@ class ExcelotronManager(QObject):
|
|
|
11553
12361
|
|
|
11554
12362
|
class MachineWindow(QMainWindow):
|
|
11555
12363
|
|
|
11556
|
-
def __init__(self, parent=None, GPU = False):
|
|
12364
|
+
def __init__(self, parent=None, GPU = False, tutorial_example = False):
|
|
11557
12365
|
super().__init__(parent)
|
|
11558
12366
|
|
|
11559
12367
|
try:
|
|
11560
12368
|
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
12369
|
+
self.tutorial_example = tutorial_example
|
|
12370
|
+
|
|
12371
|
+
if not tutorial_example:
|
|
12372
|
+
if self.parent().active_channel == 0:
|
|
12373
|
+
if self.parent().channel_data[0] is not None:
|
|
12374
|
+
try:
|
|
12375
|
+
active_data = self.parent().channel_data[0]
|
|
12376
|
+
act_channel = 0
|
|
12377
|
+
except:
|
|
12378
|
+
active_data = self.parent().channel_data[1]
|
|
12379
|
+
act_channel = 1
|
|
12380
|
+
else:
|
|
11567
12381
|
active_data = self.parent().channel_data[1]
|
|
11568
12382
|
act_channel = 1
|
|
11569
|
-
else:
|
|
11570
|
-
active_data = self.parent().channel_data[1]
|
|
11571
|
-
act_channel = 1
|
|
11572
12383
|
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
12384
|
+
try:
|
|
12385
|
+
if len(active_data.shape) == 3:
|
|
12386
|
+
array1 = np.zeros_like(active_data).astype(np.uint8)
|
|
12387
|
+
elif len(active_data.shape) == 4:
|
|
12388
|
+
array1 = np.zeros_like(active_data)[:,:,:,0].astype(np.uint8)
|
|
12389
|
+
except:
|
|
12390
|
+
print("No data in nodes channel")
|
|
12391
|
+
return
|
|
12392
|
+
|
|
12393
|
+
if not tutorial_example:
|
|
12394
|
+
self.setWindowTitle("Segmenter")
|
|
12395
|
+
else:
|
|
12396
|
+
self.setWindowTitle("Tutorial Segmenter View (This window will not actually segment)")
|
|
11581
12397
|
|
|
11582
|
-
self.setWindowTitle("Threshold")
|
|
11583
12398
|
|
|
11584
12399
|
# Create central widget and layout
|
|
11585
12400
|
central_widget = QWidget()
|
|
@@ -11600,22 +12415,23 @@ class MachineWindow(QMainWindow):
|
|
|
11600
12415
|
|
|
11601
12416
|
self.parent().pen_button.setEnabled(False)
|
|
11602
12417
|
|
|
11603
|
-
|
|
11604
|
-
|
|
12418
|
+
if not tutorial_example:
|
|
12419
|
+
array3 = np.zeros_like(array1).astype(np.uint8)
|
|
12420
|
+
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
11605
12421
|
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
12422
|
+
self.parent().load_channel(2, array1, True)
|
|
12423
|
+
# Enable the channel button
|
|
12424
|
+
# Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
|
|
12425
|
+
if not self.parent().channel_buttons[2].isEnabled():
|
|
12426
|
+
self.parent().channel_buttons[2].setEnabled(True)
|
|
12427
|
+
self.parent().channel_buttons[2].click()
|
|
12428
|
+
self.parent().delete_buttons[2].setEnabled(True)
|
|
11613
12429
|
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
12430
|
+
if len(active_data.shape) == 3:
|
|
12431
|
+
self.parent().base_colors[act_channel] = self.parent().color_dictionary['WHITE']
|
|
12432
|
+
self.parent().base_colors[2] = self.parent().color_dictionary['LIGHT_GREEN']
|
|
11617
12433
|
|
|
11618
|
-
|
|
12434
|
+
self.parent().update_display()
|
|
11619
12435
|
|
|
11620
12436
|
# Set a reasonable default size for the window
|
|
11621
12437
|
self.setMinimumWidth(600) # Increased to accommodate grouped buttons
|
|
@@ -11821,8 +12637,7 @@ class MachineWindow(QMainWindow):
|
|
|
11821
12637
|
self.num_chunks = 0
|
|
11822
12638
|
self.parent().update_display()
|
|
11823
12639
|
except:
|
|
11824
|
-
|
|
11825
|
-
traceback.print_exc()
|
|
12640
|
+
|
|
11826
12641
|
pass
|
|
11827
12642
|
|
|
11828
12643
|
except:
|
|
@@ -12221,34 +13036,43 @@ class MachineWindow(QMainWindow):
|
|
|
12221
13036
|
|
|
12222
13037
|
def closeEvent(self, event):
|
|
12223
13038
|
try:
|
|
12224
|
-
if
|
|
12225
|
-
if self.
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
self.
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
13039
|
+
if not self.tutorial_example:
|
|
13040
|
+
if self.parent() and self.parent().isVisible():
|
|
13041
|
+
if self.confirm_close_dialog():
|
|
13042
|
+
# Clean up resources before closing
|
|
13043
|
+
if self.brush_button.isChecked():
|
|
13044
|
+
self.silence_button()
|
|
13045
|
+
self.toggle_brush_mode()
|
|
13046
|
+
|
|
13047
|
+
self.parent().pen_button.setEnabled(True)
|
|
13048
|
+
self.parent().brush_mode = False
|
|
13049
|
+
|
|
13050
|
+
# Kill the segmentation thread and wait for it to finish
|
|
13051
|
+
self.kill_segmentation()
|
|
13052
|
+
time.sleep(0.2) # Give additional time for cleanup
|
|
13053
|
+
try:
|
|
13054
|
+
self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
|
|
13055
|
+
self.update_display()
|
|
13056
|
+
except:
|
|
13057
|
+
pass
|
|
13058
|
+
|
|
13059
|
+
self.parent().machine_window = None
|
|
13060
|
+
event.accept() # IMPORTANT: Accept the close event
|
|
13061
|
+
else:
|
|
13062
|
+
event.ignore() # User cancelled, ignore the close
|
|
12245
13063
|
else:
|
|
12246
|
-
|
|
13064
|
+
# Parent doesn't exist or isn't visible, just close
|
|
13065
|
+
if hasattr(self, 'parent') and self.parent():
|
|
13066
|
+
self.parent().machine_window = None
|
|
13067
|
+
event.accept()
|
|
12247
13068
|
else:
|
|
12248
|
-
|
|
12249
|
-
if
|
|
12250
|
-
self.
|
|
12251
|
-
|
|
13069
|
+
self.parent().machine_window = None
|
|
13070
|
+
if self.brush_button.isChecked():
|
|
13071
|
+
self.silence_button()
|
|
13072
|
+
self.toggle_brush_mode()
|
|
13073
|
+
self.parent().pen_button.setEnabled(True)
|
|
13074
|
+
self.parent().brush_mode = False
|
|
13075
|
+
|
|
12252
13076
|
except Exception as e:
|
|
12253
13077
|
print(f"Error in closeEvent: {e}")
|
|
12254
13078
|
# Even if there's an error, allow the window to close
|
|
@@ -12356,6 +13180,7 @@ class ThresholdWindow(QMainWindow):
|
|
|
12356
13180
|
|
|
12357
13181
|
def __init__(self, parent=None, accepted_mode=0):
|
|
12358
13182
|
super().__init__(parent)
|
|
13183
|
+
self.parent().thresh_window_ref = self
|
|
12359
13184
|
self.setWindowTitle("Threshold")
|
|
12360
13185
|
|
|
12361
13186
|
self.accepted_mode = accepted_mode
|
|
@@ -12400,19 +13225,28 @@ class ThresholdWindow(QMainWindow):
|
|
|
12400
13225
|
data = self.parent().channel_data[self.parent().active_channel]
|
|
12401
13226
|
nonzero_data = data[data != 0]
|
|
12402
13227
|
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
13228
|
+
MAX_SAMPLES_FOR_HISTOGRAM = 10_000_000 # Downsample data above this size
|
|
13229
|
+
MAX_HISTOGRAM_BINS = 512 # Maximum bins for smooth matplotlib interaction
|
|
13230
|
+
MIN_HISTOGRAM_BINS = 128 # Minimum bins for decent resolution
|
|
13231
|
+
|
|
13232
|
+
# Always compute min/max first (before any downsampling)
|
|
13233
|
+
self.data_min = np.min(nonzero_data)
|
|
13234
|
+
self.data_max = np.max(nonzero_data)
|
|
13235
|
+
self.histo_list = [self.data_min, self.data_max]
|
|
13236
|
+
|
|
13237
|
+
# Downsample data if too large
|
|
13238
|
+
if nonzero_data.size > MAX_SAMPLES_FOR_HISTOGRAM:
|
|
13239
|
+
downsample_factor = int(np.ceil(nonzero_data.size / MAX_SAMPLES_FOR_HISTOGRAM))
|
|
13240
|
+
nonzero_data_sampled = n3d.downsample(nonzero_data, downsample_factor)
|
|
12410
13241
|
else:
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
13242
|
+
nonzero_data_sampled = nonzero_data
|
|
13243
|
+
|
|
13244
|
+
# Calculate optimal bin count (capped for matplotlib performance)
|
|
13245
|
+
# Using Sturges' rule but capped to reasonable limits
|
|
13246
|
+
n_bins = int(np.ceil(np.log2(nonzero_data_sampled.size)) + 1)
|
|
13247
|
+
n_bins = np.clip(n_bins, MIN_HISTOGRAM_BINS, MAX_HISTOGRAM_BINS)
|
|
13248
|
+
|
|
13249
|
+
counts, bin_edges = np.histogram(nonzero_data_sampled, bins=n_bins, density=False)
|
|
12416
13250
|
|
|
12417
13251
|
self.bounds = True
|
|
12418
13252
|
self.parent().bounds = True
|
|
@@ -12509,10 +13343,8 @@ class ThresholdWindow(QMainWindow):
|
|
|
12509
13343
|
self.processing_cancelled.emit()
|
|
12510
13344
|
self.close()
|
|
12511
13345
|
|
|
12512
|
-
def
|
|
12513
|
-
|
|
12514
|
-
self.parent().targs = None
|
|
12515
|
-
self.parent().bounds = False
|
|
13346
|
+
def make_full_highlight(self):
|
|
13347
|
+
|
|
12516
13348
|
try: # could probably be refactored but this just handles keeping the highlight elements if the user presses X
|
|
12517
13349
|
if self.chan == 0:
|
|
12518
13350
|
if not self.bounds:
|
|
@@ -12546,6 +13378,14 @@ class ThresholdWindow(QMainWindow):
|
|
|
12546
13378
|
pass
|
|
12547
13379
|
|
|
12548
13380
|
|
|
13381
|
+
def closeEvent(self, event):
|
|
13382
|
+
self.parent().preview = False
|
|
13383
|
+
self.parent().targs = None
|
|
13384
|
+
self.parent().bounds = False
|
|
13385
|
+
self.parent().thresh_window_ref = None
|
|
13386
|
+
self.make_full_highlight()
|
|
13387
|
+
|
|
13388
|
+
|
|
12549
13389
|
def get_values_in_range_all_vols(self, chan, min_val, max_val):
|
|
12550
13390
|
output = []
|
|
12551
13391
|
if self.accepted_mode == 1:
|
|
@@ -12812,36 +13652,33 @@ class SmartDilateDialog(QDialog):
|
|
|
12812
13652
|
|
|
12813
13653
|
|
|
12814
13654
|
class DilateDialog(QDialog):
|
|
12815
|
-
def __init__(self, parent=None):
|
|
13655
|
+
def __init__(self, parent=None, args = None):
|
|
12816
13656
|
super().__init__(parent)
|
|
12817
13657
|
self.setWindowTitle("Dilate Parameters")
|
|
12818
|
-
self.setModal(
|
|
13658
|
+
self.setModal(False)
|
|
12819
13659
|
|
|
12820
13660
|
layout = QFormLayout(self)
|
|
12821
13661
|
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
if my_network.xy_scale is not None:
|
|
12826
|
-
xy_scale = f"{my_network.xy_scale}"
|
|
13662
|
+
if args:
|
|
13663
|
+
self.parent().last_dil = args[0]
|
|
13664
|
+
self.index = 1
|
|
12827
13665
|
else:
|
|
12828
|
-
|
|
13666
|
+
self.parent().last_dil = 1
|
|
13667
|
+
self.index = 0
|
|
12829
13668
|
|
|
12830
|
-
self.
|
|
12831
|
-
layout.addRow("
|
|
13669
|
+
self.amount = QLineEdit(f"{self.parent().last_dil}")
|
|
13670
|
+
layout.addRow("Dilation Radius:", self.amount)
|
|
12832
13671
|
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
else:
|
|
12836
|
-
z_scale = "1"
|
|
13672
|
+
self.xy_scale = QLineEdit("1")
|
|
13673
|
+
layout.addRow("xy_scale:", self.xy_scale)
|
|
12837
13674
|
|
|
12838
|
-
self.z_scale = QLineEdit(
|
|
13675
|
+
self.z_scale = QLineEdit("1")
|
|
12839
13676
|
layout.addRow("z_scale:", self.z_scale)
|
|
12840
13677
|
|
|
12841
13678
|
# Add mode selection dropdown
|
|
12842
13679
|
self.mode_selector = QComboBox()
|
|
12843
|
-
self.mode_selector.addItems(["
|
|
12844
|
-
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
|
|
12845
13682
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
12846
13683
|
|
|
12847
13684
|
# Add Run button
|
|
@@ -12883,13 +13720,15 @@ class DilateDialog(QDialog):
|
|
|
12883
13720
|
if active_data is None:
|
|
12884
13721
|
raise ValueError("No active image selected")
|
|
12885
13722
|
|
|
13723
|
+
self.parent().last_dil = amount
|
|
13724
|
+
|
|
12886
13725
|
if accepted_mode == 1:
|
|
12887
13726
|
dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
|
|
12888
13727
|
dialog.exec()
|
|
12889
13728
|
self.accept()
|
|
12890
13729
|
return
|
|
12891
13730
|
|
|
12892
|
-
if accepted_mode ==
|
|
13731
|
+
if accepted_mode == 0:
|
|
12893
13732
|
result = n3d.dilate_3D_dt(active_data, amount, xy_scaling = xy_scale, z_scaling = z_scale)
|
|
12894
13733
|
else:
|
|
12895
13734
|
|
|
@@ -12919,36 +13758,33 @@ class DilateDialog(QDialog):
|
|
|
12919
13758
|
)
|
|
12920
13759
|
|
|
12921
13760
|
class ErodeDialog(QDialog):
|
|
12922
|
-
def __init__(self, parent=None):
|
|
13761
|
+
def __init__(self, parent=None, args = None):
|
|
12923
13762
|
super().__init__(parent)
|
|
12924
13763
|
self.setWindowTitle("Erosion Parameters")
|
|
12925
13764
|
self.setModal(True)
|
|
12926
13765
|
|
|
12927
13766
|
layout = QFormLayout(self)
|
|
12928
13767
|
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
if my_network.xy_scale is not None:
|
|
12933
|
-
xy_scale = f"{my_network.xy_scale}"
|
|
13768
|
+
if args:
|
|
13769
|
+
self.parent().last_ero = args[0]
|
|
13770
|
+
self.index = 1
|
|
12934
13771
|
else:
|
|
12935
|
-
|
|
13772
|
+
self.parent().last_ero = 1
|
|
13773
|
+
self.index = 0
|
|
12936
13774
|
|
|
12937
|
-
self.
|
|
12938
|
-
layout.addRow("
|
|
13775
|
+
self.amount = QLineEdit(f"{self.parent().last_ero}")
|
|
13776
|
+
layout.addRow("Erosion Radius:", self.amount)
|
|
12939
13777
|
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
else:
|
|
12943
|
-
z_scale = "1"
|
|
13778
|
+
self.xy_scale = QLineEdit("1")
|
|
13779
|
+
layout.addRow("xy_scale:", self.xy_scale)
|
|
12944
13780
|
|
|
12945
|
-
self.z_scale = QLineEdit(
|
|
13781
|
+
self.z_scale = QLineEdit("1")
|
|
12946
13782
|
layout.addRow("z_scale:", self.z_scale)
|
|
12947
13783
|
|
|
12948
13784
|
# Add mode selection dropdown
|
|
12949
13785
|
self.mode_selector = QComboBox()
|
|
12950
|
-
self.mode_selector.addItems(["
|
|
12951
|
-
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
|
|
12952
13788
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
12953
13789
|
|
|
12954
13790
|
# Add Run button
|
|
@@ -12984,8 +13820,7 @@ class ErodeDialog(QDialog):
|
|
|
12984
13820
|
|
|
12985
13821
|
mode = self.mode_selector.currentIndex()
|
|
12986
13822
|
|
|
12987
|
-
if mode ==
|
|
12988
|
-
mode = 1
|
|
13823
|
+
if mode == 1:
|
|
12989
13824
|
preserve_labels = True
|
|
12990
13825
|
else:
|
|
12991
13826
|
preserve_labels = False
|
|
@@ -13007,7 +13842,7 @@ class ErodeDialog(QDialog):
|
|
|
13007
13842
|
|
|
13008
13843
|
|
|
13009
13844
|
self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
13010
|
-
|
|
13845
|
+
self.parent().last_ero = amount
|
|
13011
13846
|
self.accept()
|
|
13012
13847
|
|
|
13013
13848
|
except Exception as e:
|
|
@@ -13037,6 +13872,11 @@ class HoleDialog(QDialog):
|
|
|
13037
13872
|
self.borders.setChecked(False)
|
|
13038
13873
|
layout.addRow("Fill Small Holes Along Borders:", self.borders)
|
|
13039
13874
|
|
|
13875
|
+
self.preserve_labels = QPushButton("Preserve Labels")
|
|
13876
|
+
self.preserve_labels.setCheckable(True)
|
|
13877
|
+
self.preserve_labels.setChecked(False)
|
|
13878
|
+
layout.addRow("Preserve Labels (Slower):", self.preserve_labels)
|
|
13879
|
+
|
|
13040
13880
|
self.sep_holes = QPushButton("Seperate Hole Mask")
|
|
13041
13881
|
self.sep_holes.setCheckable(True)
|
|
13042
13882
|
self.sep_holes.setChecked(False)
|
|
@@ -13059,6 +13899,9 @@ class HoleDialog(QDialog):
|
|
|
13059
13899
|
borders = self.borders.isChecked()
|
|
13060
13900
|
headon = self.headon.isChecked()
|
|
13061
13901
|
sep_holes = self.sep_holes.isChecked()
|
|
13902
|
+
preserve_labels = self.preserve_labels.isChecked()
|
|
13903
|
+
if preserve_labels:
|
|
13904
|
+
label_copy = np.copy(active_data)
|
|
13062
13905
|
|
|
13063
13906
|
if borders:
|
|
13064
13907
|
|
|
@@ -13077,7 +13920,11 @@ class HoleDialog(QDialog):
|
|
|
13077
13920
|
fill_borders = borders
|
|
13078
13921
|
)
|
|
13079
13922
|
|
|
13923
|
+
|
|
13080
13924
|
if not sep_holes:
|
|
13925
|
+
if preserve_labels:
|
|
13926
|
+
result = sdl.smart_label(result, label_copy, directory = None, GPU = False, remove_template = True)
|
|
13927
|
+
|
|
13081
13928
|
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
13082
13929
|
else:
|
|
13083
13930
|
self.parent().load_channel(3, active_data - result, True)
|
|
@@ -13093,6 +13940,135 @@ class HoleDialog(QDialog):
|
|
|
13093
13940
|
f"Error running fill holes: {str(e)}"
|
|
13094
13941
|
)
|
|
13095
13942
|
|
|
13943
|
+
class FilamentDialog(QDialog):
|
|
13944
|
+
def __init__(self, parent=None):
|
|
13945
|
+
super().__init__(parent)
|
|
13946
|
+
self.setWindowTitle("Parameters for Vessel Tracer (Note none of these are scaled with xy or z scale properties)")
|
|
13947
|
+
self.setModal(False)
|
|
13948
|
+
|
|
13949
|
+
main_layout = QVBoxLayout(self)
|
|
13950
|
+
|
|
13951
|
+
# Speedup Group
|
|
13952
|
+
speedup_group = QGroupBox("Speedup")
|
|
13953
|
+
speedup_layout = QFormLayout()
|
|
13954
|
+
self.kernel_spacing = QLineEdit("3")
|
|
13955
|
+
speedup_layout.addRow("Kernel Spacing (1 is most accurate, can increase to speed up):", self.kernel_spacing)
|
|
13956
|
+
self.downsample_factor = QLineEdit("1")
|
|
13957
|
+
speedup_layout.addRow("Temporary Downsample Factor (Note that the below distances are not adjusted for this):", self.downsample_factor)
|
|
13958
|
+
speedup_group.setLayout(speedup_layout)
|
|
13959
|
+
main_layout.addWidget(speedup_group)
|
|
13960
|
+
|
|
13961
|
+
# Reconnection Behavior Group
|
|
13962
|
+
reconnection_group = QGroupBox("Reconnection Behavior")
|
|
13963
|
+
reconnection_layout = QFormLayout()
|
|
13964
|
+
self.max_distance = QLineEdit("20")
|
|
13965
|
+
reconnection_layout.addRow("Max Distance to Consider Connecting Filaments (Will Slow Down a lot if Large):", self.max_distance)
|
|
13966
|
+
self.gap_tolerance = QLineEdit("5")
|
|
13967
|
+
reconnection_layout.addRow("Gap Tolerance. Higher Values Increase Likelihood of Connecting over Larger Gaps:", self.gap_tolerance)
|
|
13968
|
+
self.score_threshold = QLineEdit("2")
|
|
13969
|
+
reconnection_layout.addRow("Connection Quality Threshold. Lower Values Increase Likelihood of Connecting In General, can be Negative:", self.score_threshold)
|
|
13970
|
+
reconnection_group.setLayout(reconnection_layout)
|
|
13971
|
+
main_layout.addWidget(reconnection_group)
|
|
13972
|
+
|
|
13973
|
+
# Artifact Removal Group
|
|
13974
|
+
artifact_group = QGroupBox("Artifact Removal")
|
|
13975
|
+
artifact_layout = QFormLayout()
|
|
13976
|
+
self.min_component = QLineEdit("20")
|
|
13977
|
+
artifact_layout.addRow("Minimum Component Size to Include:", self.min_component)
|
|
13978
|
+
self.blob_sphericity = QLineEdit("1.0")
|
|
13979
|
+
artifact_layout.addRow("Spherical Objects in the Output can Represent Noise. Enter a val 0 < x < 1 to consider removing spheroids. Larger vals are more spherical. 1.0 = a perfect sphere. 0.3 is usually the lower bound of a spheroid:", self.blob_sphericity)
|
|
13980
|
+
self.blob_volume = QLineEdit("200")
|
|
13981
|
+
artifact_layout.addRow("If filtering spheroids: Minimum Volume of Spheroid to Remove (Smaller spheroids may be real):", self.blob_volume)
|
|
13982
|
+
self.spine_removal = QLineEdit("0")
|
|
13983
|
+
artifact_layout.addRow("Remove Branch Spines Below this Length?", self.spine_removal)
|
|
13984
|
+
artifact_group.setLayout(artifact_layout)
|
|
13985
|
+
main_layout.addWidget(artifact_group)
|
|
13986
|
+
|
|
13987
|
+
|
|
13988
|
+
# Run Button
|
|
13989
|
+
run_button = QPushButton("Run Filament Tracer (Output Goes in Overlay 2)")
|
|
13990
|
+
run_button.clicked.connect(self.run)
|
|
13991
|
+
main_layout.addWidget(run_button)
|
|
13992
|
+
|
|
13993
|
+
|
|
13994
|
+
def run(self):
|
|
13995
|
+
|
|
13996
|
+
try:
|
|
13997
|
+
|
|
13998
|
+
from . import filaments
|
|
13999
|
+
|
|
14000
|
+
|
|
14001
|
+
kernel_spacing = int(self.kernel_spacing.text()) if self.kernel_spacing.text().strip() else 1
|
|
14002
|
+
max_distance = float(self.max_distance.text()) if self.max_distance.text().strip() else 20
|
|
14003
|
+
min_component = int(self.min_component.text()) if self.min_component.text().strip() else 20
|
|
14004
|
+
gap_tolerance = float(self.gap_tolerance.text()) if self.gap_tolerance.text().strip() else 5
|
|
14005
|
+
blob_sphericity = float(self.blob_sphericity.text()) if self.blob_sphericity.text().strip() else 1
|
|
14006
|
+
blob_volume = float(self.blob_volume.text()) if self.blob_volume.text().strip() else 200
|
|
14007
|
+
spine_removal = int(self.spine_removal.text()) if self.spine_removal.text().strip() else 0
|
|
14008
|
+
score_threshold = int(self.score_threshold.text()) if self.score_threshold.text().strip() else 0
|
|
14009
|
+
downsample_factor = int(self.downsample_factor.text()) if self.downsample_factor.text().strip() else None
|
|
14010
|
+
data = self.parent().channel_data[self.parent().active_channel]
|
|
14011
|
+
|
|
14012
|
+
if downsample_factor and downsample_factor > 1:
|
|
14013
|
+
data = n3d.downsample(data, downsample_factor)
|
|
14014
|
+
|
|
14015
|
+
result = filaments.trace(data, kernel_spacing, max_distance, min_component, gap_tolerance, blob_sphericity, blob_volume, spine_removal, score_threshold)
|
|
14016
|
+
|
|
14017
|
+
if downsample_factor and downsample_factor > 1:
|
|
14018
|
+
|
|
14019
|
+
result = n3d.upsample_with_padding(result, original_shape = self.parent().shape)
|
|
14020
|
+
|
|
14021
|
+
|
|
14022
|
+
self.parent().load_channel(3, result, True)
|
|
14023
|
+
|
|
14024
|
+
self.accept()
|
|
14025
|
+
|
|
14026
|
+
except Exception as e:
|
|
14027
|
+
import traceback
|
|
14028
|
+
print(traceback.format_exc())
|
|
14029
|
+
print(f"Error: {e}")
|
|
14030
|
+
|
|
14031
|
+
def wait_for_threshold_processing(self):
|
|
14032
|
+
"""
|
|
14033
|
+
Opens ThresholdWindow and waits for user to process the image.
|
|
14034
|
+
Returns True if completed, False if cancelled.
|
|
14035
|
+
The thresholded image will be available in the main window after completion.
|
|
14036
|
+
"""
|
|
14037
|
+
# Create event loop to wait for user
|
|
14038
|
+
loop = QEventLoop()
|
|
14039
|
+
result = {'completed': False}
|
|
14040
|
+
|
|
14041
|
+
# Create the threshold window
|
|
14042
|
+
thresh_window = ThresholdWindow(self.parent(), 0)
|
|
14043
|
+
|
|
14044
|
+
|
|
14045
|
+
# Connect signals
|
|
14046
|
+
def on_processing_complete():
|
|
14047
|
+
result['completed'] = True
|
|
14048
|
+
loop.quit()
|
|
14049
|
+
|
|
14050
|
+
def on_processing_cancelled():
|
|
14051
|
+
result['completed'] = False
|
|
14052
|
+
loop.quit()
|
|
14053
|
+
|
|
14054
|
+
thresh_window.processing_complete.connect(on_processing_complete)
|
|
14055
|
+
thresh_window.processing_cancelled.connect(on_processing_cancelled)
|
|
14056
|
+
|
|
14057
|
+
# Show window and wait
|
|
14058
|
+
thresh_window.show()
|
|
14059
|
+
thresh_window.raise_()
|
|
14060
|
+
thresh_window.activateWindow()
|
|
14061
|
+
|
|
14062
|
+
# Block until user clicks "Apply Threshold & Continue" or "Cancel"
|
|
14063
|
+
loop.exec()
|
|
14064
|
+
|
|
14065
|
+
# Clean up
|
|
14066
|
+
thresh_window.deleteLater()
|
|
14067
|
+
|
|
14068
|
+
return result['completed']
|
|
14069
|
+
|
|
14070
|
+
|
|
14071
|
+
|
|
13096
14072
|
class MaskDialog(QDialog):
|
|
13097
14073
|
|
|
13098
14074
|
def __init__(self, parent=None):
|
|
@@ -13422,7 +14398,13 @@ class SkeletonizeDialog(QDialog):
|
|
|
13422
14398
|
# auto checkbox (default True)
|
|
13423
14399
|
self.auto = QPushButton("Auto")
|
|
13424
14400
|
self.auto.setCheckable(True)
|
|
13425
|
-
|
|
14401
|
+
try:
|
|
14402
|
+
if self.shape[0] == 1:
|
|
14403
|
+
self.auto.setChecked(False)
|
|
14404
|
+
else:
|
|
14405
|
+
self.auto.setChecked(True)
|
|
14406
|
+
except:
|
|
14407
|
+
self.auto.setChecked(True)
|
|
13426
14408
|
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
13427
14409
|
|
|
13428
14410
|
# Add Run button
|
|
@@ -13478,6 +14460,86 @@ class SkeletonizeDialog(QDialog):
|
|
|
13478
14460
|
f"Error running skeletonize: {str(e)}"
|
|
13479
14461
|
)
|
|
13480
14462
|
|
|
14463
|
+
|
|
14464
|
+
class BranchStatDialog(QDialog):
|
|
14465
|
+
|
|
14466
|
+
def __init__(self, parent=None):
|
|
14467
|
+
super().__init__(parent)
|
|
14468
|
+
self.setWindowTitle("Make sure branches are labeled first (Image -> Generate -> Label Branches)")
|
|
14469
|
+
self.setModal(True)
|
|
14470
|
+
|
|
14471
|
+
layout = QFormLayout(self)
|
|
14472
|
+
|
|
14473
|
+
info_label = QLabel("Skeletonization Params for Getting Branch Stats, Make sure xy and z scale are set correctly in properties")
|
|
14474
|
+
layout.addRow(info_label)
|
|
14475
|
+
|
|
14476
|
+
self.remove = QLineEdit("0")
|
|
14477
|
+
layout.addRow("Remove Branches Pixel Length (int):", self.remove)
|
|
14478
|
+
|
|
14479
|
+
# auto checkbox (default True)
|
|
14480
|
+
self.auto = QPushButton("Auto")
|
|
14481
|
+
self.auto.setCheckable(True)
|
|
14482
|
+
try:
|
|
14483
|
+
if self.shape[0] == 1:
|
|
14484
|
+
self.auto.setChecked(False)
|
|
14485
|
+
else:
|
|
14486
|
+
self.auto.setChecked(True)
|
|
14487
|
+
except:
|
|
14488
|
+
self.auto.setChecked(True)
|
|
14489
|
+
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
14490
|
+
|
|
14491
|
+
# Add Run button
|
|
14492
|
+
run_button = QPushButton("Get Branchstats (For Active Image)")
|
|
14493
|
+
run_button.clicked.connect(self.run)
|
|
14494
|
+
layout.addRow(run_button)
|
|
14495
|
+
|
|
14496
|
+
def run(self):
|
|
14497
|
+
|
|
14498
|
+
try:
|
|
14499
|
+
|
|
14500
|
+
# Get branch removal
|
|
14501
|
+
try:
|
|
14502
|
+
remove = int(self.remove.text()) if self.remove.text() else 0
|
|
14503
|
+
except ValueError:
|
|
14504
|
+
remove = 0
|
|
14505
|
+
|
|
14506
|
+
auto = self.auto.isChecked()
|
|
14507
|
+
|
|
14508
|
+
# Get the active channel data from parent
|
|
14509
|
+
active_data = np.copy(self.parent().channel_data[self.parent().active_channel])
|
|
14510
|
+
if active_data is None:
|
|
14511
|
+
raise ValueError("No active image selected")
|
|
14512
|
+
|
|
14513
|
+
if auto:
|
|
14514
|
+
active_data = n3d.skeletonize(active_data)
|
|
14515
|
+
active_data = n3d.fill_holes_3d(active_data)
|
|
14516
|
+
|
|
14517
|
+
active_data = n3d.skeletonize(
|
|
14518
|
+
active_data
|
|
14519
|
+
)
|
|
14520
|
+
|
|
14521
|
+
if remove > 0:
|
|
14522
|
+
active_data = n3d.remove_branches_new(active_data, remove)
|
|
14523
|
+
active_data = n3d.dilate_3D(active_data, 3, 3, 3)
|
|
14524
|
+
active_data = n3d.skeletonize(active_data)
|
|
14525
|
+
|
|
14526
|
+
active_data = active_data * self.parent().channel_data[self.parent().active_channel]
|
|
14527
|
+
len_dict, tortuosity_dict, angle_dict = n3d.compute_optional_branchstats(None, active_data, None, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
14528
|
+
|
|
14529
|
+
if self.parent().active_channel == 0:
|
|
14530
|
+
self.parent().branch_dict[0] = [len_dict, tortuosity_dict]
|
|
14531
|
+
elif self.parent().active_channel == 1:
|
|
14532
|
+
self.parent().branch_dict[1] = [len_dict, tortuosity_dict]
|
|
14533
|
+
|
|
14534
|
+
self.parent().format_for_upperright_table(len_dict, 'BranchID', 'Length (Scaled)', 'Branch Lengths')
|
|
14535
|
+
self.parent().format_for_upperright_table(tortuosity_dict, 'BranchID', 'Tortuosity', 'Branch Tortuosities')
|
|
14536
|
+
|
|
14537
|
+
|
|
14538
|
+
self.accept()
|
|
14539
|
+
|
|
14540
|
+
except Exception as e:
|
|
14541
|
+
print(f"Error: {e}")
|
|
14542
|
+
|
|
13481
14543
|
class DistanceDialog(QDialog):
|
|
13482
14544
|
def __init__(self, parent=None):
|
|
13483
14545
|
super().__init__(parent)
|
|
@@ -13596,11 +14658,6 @@ class WatershedDialog(QDialog):
|
|
|
13596
14658
|
self.setModal(True)
|
|
13597
14659
|
|
|
13598
14660
|
layout = QFormLayout(self)
|
|
13599
|
-
|
|
13600
|
-
# Directory (empty by default)
|
|
13601
|
-
self.directory = QLineEdit()
|
|
13602
|
-
self.directory.setPlaceholderText("Leave empty for None")
|
|
13603
|
-
layout.addRow("Output Directory:", self.directory)
|
|
13604
14661
|
|
|
13605
14662
|
try:
|
|
13606
14663
|
|
|
@@ -13651,7 +14708,7 @@ class WatershedDialog(QDialog):
|
|
|
13651
14708
|
def run_watershed(self):
|
|
13652
14709
|
try:
|
|
13653
14710
|
# Get directory (None if empty)
|
|
13654
|
-
directory =
|
|
14711
|
+
directory = None
|
|
13655
14712
|
|
|
13656
14713
|
# Get proportion (0.1 if empty or invalid)
|
|
13657
14714
|
try:
|
|
@@ -13895,7 +14952,7 @@ class GenNodesDialog(QDialog):
|
|
|
13895
14952
|
def __init__(self, parent=None, down_factor=None, called=False):
|
|
13896
14953
|
super().__init__(parent)
|
|
13897
14954
|
self.setWindowTitle("Create Nodes from Edge Vertices")
|
|
13898
|
-
self.setModal(
|
|
14955
|
+
self.setModal(False)
|
|
13899
14956
|
|
|
13900
14957
|
# Main layout
|
|
13901
14958
|
main_layout = QVBoxLayout(self)
|
|
@@ -13919,15 +14976,15 @@ class GenNodesDialog(QDialog):
|
|
|
13919
14976
|
self.cubic = QPushButton("Cubic Downsample")
|
|
13920
14977
|
self.cubic.setCheckable(True)
|
|
13921
14978
|
self.cubic.setChecked(False)
|
|
13922
|
-
process_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
|
|
13923
|
-
process_layout.addWidget(self.cubic, 1, 1)
|
|
14979
|
+
#process_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
|
|
14980
|
+
#process_layout.addWidget(self.cubic, 1, 1)
|
|
13924
14981
|
|
|
13925
14982
|
# Fast dilation checkbox
|
|
13926
14983
|
self.fast_dil = QPushButton("Fast-Dil")
|
|
13927
14984
|
self.fast_dil.setCheckable(True)
|
|
13928
14985
|
self.fast_dil.setChecked(True)
|
|
13929
|
-
process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 2, 0)
|
|
13930
|
-
process_layout.addWidget(self.fast_dil, 2, 1)
|
|
14986
|
+
#process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 2, 0)
|
|
14987
|
+
#process_layout.addWidget(self.fast_dil, 2, 1)
|
|
13931
14988
|
|
|
13932
14989
|
process_group.setLayout(process_layout)
|
|
13933
14990
|
main_layout.addWidget(process_group)
|
|
@@ -13936,17 +14993,17 @@ class GenNodesDialog(QDialog):
|
|
|
13936
14993
|
self.cubic = down_factor[1]
|
|
13937
14994
|
|
|
13938
14995
|
# Fast dilation checkbox (still needed even if down_factor is provided)
|
|
13939
|
-
process_group = QGroupBox("Processing Options")
|
|
13940
|
-
process_layout = QGridLayout()
|
|
14996
|
+
#process_group = QGroupBox("Processing Options")
|
|
14997
|
+
#process_layout = QGridLayout()
|
|
13941
14998
|
|
|
13942
14999
|
self.fast_dil = QPushButton("Fast-Dil")
|
|
13943
15000
|
self.fast_dil.setCheckable(True)
|
|
13944
15001
|
self.fast_dil.setChecked(True)
|
|
13945
|
-
process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 0, 0)
|
|
13946
|
-
process_layout.addWidget(self.fast_dil, 0, 1)
|
|
15002
|
+
#process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 0, 0)
|
|
15003
|
+
#process_layout.addWidget(self.fast_dil, 0, 1)
|
|
13947
15004
|
|
|
13948
|
-
process_group.setLayout(process_layout)
|
|
13949
|
-
main_layout.addWidget(process_group)
|
|
15005
|
+
#process_group.setLayout(process_layout)
|
|
15006
|
+
#main_layout.addWidget(process_group)
|
|
13950
15007
|
|
|
13951
15008
|
# --- Recommended Corrections Group ---
|
|
13952
15009
|
rec_group = QGroupBox("Recommended Corrections")
|
|
@@ -13979,8 +15036,8 @@ class GenNodesDialog(QDialog):
|
|
|
13979
15036
|
|
|
13980
15037
|
# Max volume
|
|
13981
15038
|
self.max_vol = QLineEdit("0")
|
|
13982
|
-
opt_layout.addWidget(QLabel("Maximum Voxel Volume to Retain (Compensates for skeleton looping):"), 0, 0)
|
|
13983
|
-
opt_layout.addWidget(self.max_vol, 0, 1)
|
|
15039
|
+
#opt_layout.addWidget(QLabel("Maximum Voxel Volume to Retain (Compensates for skeleton looping):"), 0, 0)
|
|
15040
|
+
#opt_layout.addWidget(self.max_vol, 0, 1)
|
|
13984
15041
|
|
|
13985
15042
|
# Component dilation
|
|
13986
15043
|
self.comp_dil = QLineEdit("0")
|
|
@@ -14056,6 +15113,8 @@ class GenNodesDialog(QDialog):
|
|
|
14056
15113
|
|
|
14057
15114
|
fastdil = self.fast_dil.isChecked()
|
|
14058
15115
|
|
|
15116
|
+
if down_factor > 1:
|
|
15117
|
+
my_network.edges = n3d.downsample(my_network.edges, down_factor)
|
|
14059
15118
|
|
|
14060
15119
|
if auto:
|
|
14061
15120
|
my_network.edges = n3d.skeletonize(my_network.edges)
|
|
@@ -14067,11 +15126,9 @@ class GenNodesDialog(QDialog):
|
|
|
14067
15126
|
max_vol=max_vol,
|
|
14068
15127
|
branch_removal=branch_removal,
|
|
14069
15128
|
comp_dil=comp_dil,
|
|
14070
|
-
down_factor=down_factor,
|
|
14071
15129
|
order = order,
|
|
14072
15130
|
return_skele = True,
|
|
14073
15131
|
fastdil = fastdil
|
|
14074
|
-
|
|
14075
15132
|
)
|
|
14076
15133
|
|
|
14077
15134
|
if down_factor > 0 and not self.called:
|
|
@@ -14122,10 +15179,10 @@ class GenNodesDialog(QDialog):
|
|
|
14122
15179
|
|
|
14123
15180
|
class BranchDialog(QDialog):
|
|
14124
15181
|
|
|
14125
|
-
def __init__(self, parent=None, called = False):
|
|
15182
|
+
def __init__(self, parent=None, called = False, tutorial_example = False):
|
|
14126
15183
|
super().__init__(parent)
|
|
14127
15184
|
self.setWindowTitle("Label Branches (of edges)")
|
|
14128
|
-
self.setModal(
|
|
15185
|
+
self.setModal(False)
|
|
14129
15186
|
|
|
14130
15187
|
# Main layout
|
|
14131
15188
|
main_layout = QVBoxLayout(self)
|
|
@@ -14138,33 +15195,40 @@ class BranchDialog(QDialog):
|
|
|
14138
15195
|
self.fix = QPushButton("Auto-Correct 1")
|
|
14139
15196
|
self.fix.setCheckable(True)
|
|
14140
15197
|
self.fix.setChecked(False)
|
|
14141
|
-
correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
|
|
14142
|
-
correction_layout.addWidget(self.fix, 0, 1)
|
|
15198
|
+
#correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
|
|
15199
|
+
#correction_layout.addWidget(self.fix, 0, 1)
|
|
14143
15200
|
|
|
14144
15201
|
# Fix value
|
|
14145
15202
|
self.fix_val = QLineEdit('4')
|
|
14146
|
-
correction_layout.addWidget(QLabel("Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
|
|
14147
|
-
correction_layout.addWidget(self.fix_val, 1, 1)
|
|
15203
|
+
#correction_layout.addWidget(QLabel("(For Auto-Correct 1) Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
|
|
15204
|
+
#correction_layout.addWidget(self.fix_val, 1, 1)
|
|
14148
15205
|
|
|
14149
15206
|
# Seed
|
|
14150
15207
|
self.seed = QLineEdit('')
|
|
14151
|
-
correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
|
|
14152
|
-
correction_layout.addWidget(self.seed, 2, 1)
|
|
15208
|
+
#correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
|
|
15209
|
+
#correction_layout.addWidget(self.seed, 2, 1)
|
|
14153
15210
|
|
|
14154
|
-
self.fix2 = QPushButton("Auto-Correct
|
|
15211
|
+
self.fix2 = QPushButton("Auto-Correct Internal Branches")
|
|
14155
15212
|
self.fix2.setCheckable(True)
|
|
14156
15213
|
self.fix2.setChecked(True)
|
|
14157
15214
|
correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Internal Labels: "), 3, 0)
|
|
14158
15215
|
correction_layout.addWidget(self.fix2, 3, 1)
|
|
14159
15216
|
|
|
14160
|
-
self.fix3 = QPushButton("
|
|
15217
|
+
self.fix3 = QPushButton("Auto-Correct Nontouching Branches")
|
|
14161
15218
|
self.fix3.setCheckable(True)
|
|
14162
|
-
|
|
14163
|
-
|
|
14164
|
-
else:
|
|
14165
|
-
self.fix3.setChecked(False)
|
|
14166
|
-
correction_layout.addWidget(QLabel("Split Nontouching Branches? (Useful if branch pruning - may want to threshold out small, split branches after): "), 4, 0)
|
|
15219
|
+
self.fix3.setChecked(True)
|
|
15220
|
+
correction_layout.addWidget(QLabel("Auto-Correct Nontouching Branches?: "), 4, 0)
|
|
14167
15221
|
correction_layout.addWidget(self.fix3, 4, 1)
|
|
15222
|
+
|
|
15223
|
+
self.fix4 = QPushButton("Auto-Attempt to Reunify Main Branches?")
|
|
15224
|
+
self.fix4.setCheckable(True)
|
|
15225
|
+
self.fix4.setChecked(False)
|
|
15226
|
+
correction_layout.addWidget(QLabel("Reunify Main Branches: "), 5, 0)
|
|
15227
|
+
correction_layout.addWidget(self.fix4, 5, 1)
|
|
15228
|
+
|
|
15229
|
+
self.fix4_val = QLineEdit('10')
|
|
15230
|
+
correction_layout.addWidget(QLabel("(For Reunify) Minimum Score to Merge? (Lower vals = More mergers, can be negative):"), 6, 0)
|
|
15231
|
+
correction_layout.addWidget(self.fix4_val, 6, 1)
|
|
14168
15232
|
|
|
14169
15233
|
correction_group.setLayout(correction_layout)
|
|
14170
15234
|
main_layout.addWidget(correction_group)
|
|
@@ -14182,8 +15246,8 @@ class BranchDialog(QDialog):
|
|
|
14182
15246
|
self.cubic = QPushButton("Cubic Downsample")
|
|
14183
15247
|
self.cubic.setCheckable(True)
|
|
14184
15248
|
self.cubic.setChecked(False)
|
|
14185
|
-
processing_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
|
|
14186
|
-
processing_layout.addWidget(self.cubic, 1, 1)
|
|
15249
|
+
#processing_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
|
|
15250
|
+
#processing_layout.addWidget(self.cubic, 1, 1)
|
|
14187
15251
|
|
|
14188
15252
|
processing_group.setLayout(processing_layout)
|
|
14189
15253
|
main_layout.addWidget(processing_group)
|
|
@@ -14191,20 +15255,27 @@ class BranchDialog(QDialog):
|
|
|
14191
15255
|
# --- Misc Options Group ---
|
|
14192
15256
|
misc_group = QGroupBox("Misc Options")
|
|
14193
15257
|
misc_layout = QGridLayout()
|
|
15258
|
+
|
|
15259
|
+
# optional computation checkbox
|
|
15260
|
+
self.compute = QPushButton("Branch Stats")
|
|
15261
|
+
self.compute.setCheckable(True)
|
|
15262
|
+
self.compute.setChecked(True)
|
|
15263
|
+
misc_layout.addWidget(QLabel("Compute Branch Stats (Branch Lengths, Tortuosity. Set xy_scale and z_scale in properties first if real distances are desired.):"), 0, 0)
|
|
15264
|
+
misc_layout.addWidget(self.compute, 0, 1)
|
|
14194
15265
|
|
|
14195
15266
|
# Nodes checkbox
|
|
14196
15267
|
self.nodes = QPushButton("Generate Nodes")
|
|
14197
15268
|
self.nodes.setCheckable(True)
|
|
14198
15269
|
self.nodes.setChecked(True)
|
|
14199
|
-
misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"),
|
|
14200
|
-
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)
|
|
14201
15272
|
|
|
14202
15273
|
# GPU checkbox
|
|
14203
15274
|
self.GPU = QPushButton("GPU")
|
|
14204
15275
|
self.GPU.setCheckable(True)
|
|
14205
15276
|
self.GPU.setChecked(False)
|
|
14206
|
-
misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"),
|
|
14207
|
-
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)
|
|
14208
15279
|
|
|
14209
15280
|
misc_group.setLayout(misc_layout)
|
|
14210
15281
|
main_layout.addWidget(misc_group)
|
|
@@ -14214,7 +15285,7 @@ class BranchDialog(QDialog):
|
|
|
14214
15285
|
run_button.clicked.connect(self.branch_label)
|
|
14215
15286
|
main_layout.addWidget(run_button)
|
|
14216
15287
|
|
|
14217
|
-
if self.parent().channel_data[0] is not None or self.parent().channel_data[3] is not None:
|
|
15288
|
+
if (self.parent().channel_data[0] is not None or self.parent().channel_data[3] is not None) and not tutorial_example:
|
|
14218
15289
|
QMessageBox.critical(
|
|
14219
15290
|
self,
|
|
14220
15291
|
"Alert",
|
|
@@ -14236,8 +15307,11 @@ class BranchDialog(QDialog):
|
|
|
14236
15307
|
fix = self.fix.isChecked()
|
|
14237
15308
|
fix2 = self.fix2.isChecked()
|
|
14238
15309
|
fix3 = self.fix3.isChecked()
|
|
15310
|
+
fix4 = self.fix4.isChecked()
|
|
14239
15311
|
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
15312
|
+
fix4_val = float(self.fix4_val.text()) if self.fix4_val.text() else 10
|
|
14240
15313
|
seed = int(self.seed.text()) if self.seed.text() else None
|
|
15314
|
+
compute = self.compute.isChecked()
|
|
14241
15315
|
|
|
14242
15316
|
if my_network.edges is None and my_network.nodes is not None:
|
|
14243
15317
|
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
@@ -14254,7 +15328,12 @@ class BranchDialog(QDialog):
|
|
|
14254
15328
|
|
|
14255
15329
|
if my_network.edges is not None and my_network.nodes is not None and my_network.id_overlay is not None:
|
|
14256
15330
|
|
|
14257
|
-
|
|
15331
|
+
if fix4:
|
|
15332
|
+
unify = True
|
|
15333
|
+
else:
|
|
15334
|
+
unify = False
|
|
15335
|
+
|
|
15336
|
+
output, verts, skeleton, endpoints = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape, compute = compute, unify = unify, union_val = fix4_val, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
14258
15337
|
|
|
14259
15338
|
if fix2:
|
|
14260
15339
|
|
|
@@ -14291,7 +15370,23 @@ class BranchDialog(QDialog):
|
|
|
14291
15370
|
|
|
14292
15371
|
if fix3:
|
|
14293
15372
|
|
|
14294
|
-
output = self.parent().separate_nontouching_objects(output, max_val=np.max(output))
|
|
15373
|
+
output = self.parent().separate_nontouching_objects(output, max_val=np.max(output), branches = True)
|
|
15374
|
+
|
|
15375
|
+
if compute:
|
|
15376
|
+
if skeleton.shape != output.shape:
|
|
15377
|
+
print("Since downsampling was applied, skipping branchstats. Please use 'Analyze -> Stats -> Calculate Branch Stats' after this to find branch stats.")
|
|
15378
|
+
else:
|
|
15379
|
+
labeled_image = (skeleton != 0) * output
|
|
15380
|
+
len_dict, tortuosity_dict, angle_dict = n3d.compute_optional_branchstats(verts, labeled_image, endpoints, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
15381
|
+
self.parent().branch_dict[1] = [len_dict, tortuosity_dict]
|
|
15382
|
+
#max_length = max(len(v) for v in angle_dict.values())
|
|
15383
|
+
#title = [str(i+1) if i < 2 else i+1 for i in range(max_length)]
|
|
15384
|
+
|
|
15385
|
+
#del labeled_image
|
|
15386
|
+
|
|
15387
|
+
self.parent().format_for_upperright_table(len_dict, 'BranchID', 'Length (Scaled)', 'Branch Lengths')
|
|
15388
|
+
self.parent().format_for_upperright_table(tortuosity_dict, 'BranchID', 'Tortuosity', 'Branch Tortuosities')
|
|
15389
|
+
#self.parent().format_for_upperright_table(angle_dict, 'Vertex ID', title, 'Branch Angles')
|
|
14295
15390
|
|
|
14296
15391
|
|
|
14297
15392
|
if down_factor is not None:
|
|
@@ -14467,7 +15562,7 @@ class ModifyDialog(QDialog):
|
|
|
14467
15562
|
def __init__(self, parent=None):
|
|
14468
15563
|
super().__init__(parent)
|
|
14469
15564
|
self.setWindowTitle("Modify Network Qualities")
|
|
14470
|
-
self.setModal(
|
|
15565
|
+
self.setModal(False)
|
|
14471
15566
|
layout = QFormLayout(self)
|
|
14472
15567
|
|
|
14473
15568
|
self.revid = QPushButton("Remove Unassigned")
|
|
@@ -14692,10 +15787,6 @@ class CentroidDialog(QDialog):
|
|
|
14692
15787
|
|
|
14693
15788
|
layout = QFormLayout(self)
|
|
14694
15789
|
|
|
14695
|
-
self.directory = QLineEdit()
|
|
14696
|
-
self.directory.setPlaceholderText("Leave empty for active directory")
|
|
14697
|
-
layout.addRow("Output Directory:", self.directory)
|
|
14698
|
-
|
|
14699
15790
|
self.downsample = QLineEdit("1")
|
|
14700
15791
|
layout.addRow("Downsample Factor:", self.downsample)
|
|
14701
15792
|
|
|
@@ -14725,7 +15816,7 @@ class CentroidDialog(QDialog):
|
|
|
14725
15816
|
ignore_empty = self.ignore_empty.isChecked()
|
|
14726
15817
|
|
|
14727
15818
|
# Get directory (None if empty)
|
|
14728
|
-
directory =
|
|
15819
|
+
directory = None
|
|
14729
15820
|
|
|
14730
15821
|
# Get downsample
|
|
14731
15822
|
try:
|
|
@@ -14806,7 +15897,6 @@ class CentroidDialog(QDialog):
|
|
|
14806
15897
|
|
|
14807
15898
|
class CalcAllDialog(QDialog):
|
|
14808
15899
|
# Class variables to store previous settings
|
|
14809
|
-
prev_directory = ""
|
|
14810
15900
|
prev_search = ""
|
|
14811
15901
|
prev_diledge = ""
|
|
14812
15902
|
prev_down_factor = ""
|
|
@@ -14823,7 +15913,7 @@ class CalcAllDialog(QDialog):
|
|
|
14823
15913
|
def __init__(self, parent=None):
|
|
14824
15914
|
super().__init__(parent)
|
|
14825
15915
|
self.setWindowTitle("Calculate Connectivity Network Parameters")
|
|
14826
|
-
self.setModal(
|
|
15916
|
+
self.setModal(False)
|
|
14827
15917
|
|
|
14828
15918
|
# Main layout
|
|
14829
15919
|
main_layout = QVBoxLayout(self)
|
|
@@ -14859,7 +15949,7 @@ class CalcAllDialog(QDialog):
|
|
|
14859
15949
|
|
|
14860
15950
|
self.other_nodes = QLineEdit(self.prev_other_nodes)
|
|
14861
15951
|
self.other_nodes.setPlaceholderText("Leave empty for None")
|
|
14862
|
-
optional_layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
|
|
15952
|
+
#optional_layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
|
|
14863
15953
|
|
|
14864
15954
|
self.remove_trunk = QLineEdit(self.prev_remove_trunk)
|
|
14865
15955
|
self.remove_trunk.setPlaceholderText("Leave empty for 0")
|
|
@@ -14882,17 +15972,17 @@ class CalcAllDialog(QDialog):
|
|
|
14882
15972
|
|
|
14883
15973
|
self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
|
|
14884
15974
|
self.GPU_downsample.setPlaceholderText("Leave empty for None")
|
|
14885
|
-
speedup_layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
|
|
15975
|
+
#speedup_layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
|
|
14886
15976
|
|
|
14887
15977
|
self.gpu = QPushButton("GPU")
|
|
14888
15978
|
self.gpu.setCheckable(True)
|
|
14889
15979
|
self.gpu.setChecked(self.prev_gpu)
|
|
14890
|
-
speedup_layout.addRow("Use GPU:", self.gpu)
|
|
15980
|
+
#speedup_layout.addRow("Use GPU:", self.gpu)
|
|
14891
15981
|
|
|
14892
15982
|
self.fastdil = QPushButton("Fast Dilate")
|
|
14893
15983
|
self.fastdil.setCheckable(True)
|
|
14894
15984
|
self.fastdil.setChecked(self.prev_fastdil)
|
|
14895
|
-
speedup_layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
|
|
15985
|
+
#speedup_layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
|
|
14896
15986
|
|
|
14897
15987
|
main_layout.addWidget(speedup_group)
|
|
14898
15988
|
|
|
@@ -14900,10 +15990,6 @@ class CalcAllDialog(QDialog):
|
|
|
14900
15990
|
output_group = QGroupBox("Output Options")
|
|
14901
15991
|
output_layout = QFormLayout(output_group)
|
|
14902
15992
|
|
|
14903
|
-
self.directory = QLineEdit(self.prev_directory)
|
|
14904
|
-
self.directory.setPlaceholderText("Will Have to Save Manually If Empty")
|
|
14905
|
-
output_layout.addRow("Output Directory:", self.directory)
|
|
14906
|
-
|
|
14907
15993
|
self.overlays = QPushButton("Overlays")
|
|
14908
15994
|
self.overlays.setCheckable(True)
|
|
14909
15995
|
self.overlays.setChecked(self.prev_overlays)
|
|
@@ -14925,7 +16011,7 @@ class CalcAllDialog(QDialog):
|
|
|
14925
16011
|
|
|
14926
16012
|
try:
|
|
14927
16013
|
# Get directory (None if empty)
|
|
14928
|
-
directory =
|
|
16014
|
+
directory = None
|
|
14929
16015
|
|
|
14930
16016
|
# Get xy_scale and z_scale (1 if empty or invalid)
|
|
14931
16017
|
try:
|
|
@@ -15002,7 +16088,6 @@ class CalcAllDialog(QDialog):
|
|
|
15002
16088
|
)
|
|
15003
16089
|
|
|
15004
16090
|
# Store current values as previous values
|
|
15005
|
-
CalcAllDialog.prev_directory = self.directory.text()
|
|
15006
16091
|
CalcAllDialog.prev_search = self.search.text()
|
|
15007
16092
|
CalcAllDialog.prev_diledge = self.diledge.text()
|
|
15008
16093
|
CalcAllDialog.prev_down_factor = self.down_factor.text()
|
|
@@ -15106,10 +16191,10 @@ class CalcAllDialog(QDialog):
|
|
|
15106
16191
|
|
|
15107
16192
|
|
|
15108
16193
|
class ProxDialog(QDialog):
|
|
15109
|
-
def __init__(self, parent=None):
|
|
16194
|
+
def __init__(self, parent=None, tutorial_example = False):
|
|
15110
16195
|
super().__init__(parent)
|
|
15111
16196
|
self.setWindowTitle("Calculate Proximity Network")
|
|
15112
|
-
self.setModal(
|
|
16197
|
+
self.setModal(False)
|
|
15113
16198
|
|
|
15114
16199
|
# Main layout
|
|
15115
16200
|
main_layout = QVBoxLayout(self)
|
|
@@ -15145,6 +16230,11 @@ class ProxDialog(QDialog):
|
|
|
15145
16230
|
self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
|
|
15146
16231
|
self.id_selector.setCurrentIndex(0) # Default to Mode 1
|
|
15147
16232
|
mode_layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
|
|
16233
|
+
elif tutorial_example:
|
|
16234
|
+
self.id_selector = QComboBox()
|
|
16235
|
+
self.id_selector.addItems(['None'] + ['Example Identity A', 'Example Identity B', 'Example Identity C', 'etc...'])
|
|
16236
|
+
self.id_selector.setCurrentIndex(0) # Default to Mode 1
|
|
16237
|
+
mode_layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
|
|
15148
16238
|
else:
|
|
15149
16239
|
self.id_selector = None
|
|
15150
16240
|
|
|
@@ -15154,10 +16244,6 @@ class ProxDialog(QDialog):
|
|
|
15154
16244
|
output_group = QGroupBox("Output Options")
|
|
15155
16245
|
output_layout = QFormLayout(output_group)
|
|
15156
16246
|
|
|
15157
|
-
self.directory = QLineEdit('')
|
|
15158
|
-
self.directory.setPlaceholderText("Leave empty for 'my_network'")
|
|
15159
|
-
output_layout.addRow("Output Directory:", self.directory)
|
|
15160
|
-
|
|
15161
16247
|
self.overlays = QPushButton("Overlays")
|
|
15162
16248
|
self.overlays.setCheckable(True)
|
|
15163
16249
|
self.overlays.setChecked(True)
|
|
@@ -15183,7 +16269,7 @@ class ProxDialog(QDialog):
|
|
|
15183
16269
|
self.fastdil = QPushButton("Fast Dilate")
|
|
15184
16270
|
self.fastdil.setCheckable(True)
|
|
15185
16271
|
self.fastdil.setChecked(False)
|
|
15186
|
-
speedup_layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
|
|
16272
|
+
#speedup_layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
|
|
15187
16273
|
|
|
15188
16274
|
main_layout.addWidget(speedup_group)
|
|
15189
16275
|
|
|
@@ -15209,10 +16295,8 @@ class ProxDialog(QDialog):
|
|
|
15209
16295
|
else:
|
|
15210
16296
|
targets = None
|
|
15211
16297
|
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
except:
|
|
15215
|
-
directory = None
|
|
16298
|
+
directory = None
|
|
16299
|
+
|
|
15216
16300
|
|
|
15217
16301
|
# Get xy_scale and z_scale (1 if empty or invalid)
|
|
15218
16302
|
try:
|
|
@@ -15962,7 +17046,186 @@ class HistogramSelector(QWidget):
|
|
|
15962
17046
|
except Exception as e:
|
|
15963
17047
|
print(f"Error generating dispersion histogram: {e}")
|
|
15964
17048
|
|
|
17049
|
+
class TutorialSelectionDialog(QWidget):
|
|
17050
|
+
"""Dialog for selecting which tutorial to run"""
|
|
17051
|
+
|
|
17052
|
+
def __init__(self, window, parent=None):
|
|
17053
|
+
super().__init__(parent)
|
|
17054
|
+
self.window = window
|
|
17055
|
+
self.setWindowTitle("NetTracer3D Tutorials")
|
|
17056
|
+
self.setWindowFlags(Qt.WindowType.Window)
|
|
17057
|
+
self.setGeometry(200, 200, 400, 300)
|
|
17058
|
+
|
|
17059
|
+
layout = QVBoxLayout(self)
|
|
17060
|
+
|
|
17061
|
+
# Title
|
|
17062
|
+
title = QLabel("Select a Tutorial")
|
|
17063
|
+
title_font = QFont("Arial", 16, QFont.Weight.Bold)
|
|
17064
|
+
title.setFont(title_font)
|
|
17065
|
+
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
17066
|
+
layout.addWidget(title)
|
|
17067
|
+
|
|
17068
|
+
# Description
|
|
17069
|
+
desc = QLabel("Choose a tutorial to learn about different features of NetTracer3D:")
|
|
17070
|
+
desc.setWordWrap(True)
|
|
17071
|
+
desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
17072
|
+
layout.addWidget(desc)
|
|
17073
|
+
|
|
17074
|
+
layout.addSpacing(20)
|
|
17075
|
+
|
|
17076
|
+
# Tutorial buttons
|
|
17077
|
+
|
|
17078
|
+
intro_btn = QPushButton("Intro")
|
|
17079
|
+
intro_btn.setMinimumHeight(50)
|
|
17080
|
+
intro_btn.clicked.connect(self.start_intro)
|
|
17081
|
+
layout.addWidget(intro_btn)
|
|
17082
|
+
|
|
17083
|
+
basics_btn = QPushButton("Basic Interface Tour")
|
|
17084
|
+
basics_btn.setMinimumHeight(50)
|
|
17085
|
+
basics_btn.clicked.connect(self.start_basics_tutorial)
|
|
17086
|
+
layout.addWidget(basics_btn)
|
|
17087
|
+
|
|
17088
|
+
image_btn = QPushButton("Visualization Control Overview")
|
|
17089
|
+
image_btn.setMinimumHeight(50)
|
|
17090
|
+
image_btn.clicked.connect(self.start_image_tutorial)
|
|
17091
|
+
layout.addWidget(image_btn)
|
|
17092
|
+
|
|
17093
|
+
file_btn = QPushButton("Saving/Loading Data and Assigning Node Identities")
|
|
17094
|
+
file_btn.setMinimumHeight(50)
|
|
17095
|
+
file_btn.clicked.connect(self.start_file)
|
|
17096
|
+
layout.addWidget(file_btn)
|
|
17097
|
+
|
|
17098
|
+
seg_btn = QPushButton("Segmenting Data")
|
|
17099
|
+
seg_btn.setMinimumHeight(50)
|
|
17100
|
+
seg_btn.clicked.connect(self.start_segment)
|
|
17101
|
+
layout.addWidget(seg_btn)
|
|
17102
|
+
|
|
17103
|
+
con_btn = QPushButton("1. Creating 'Connectivity Networks'")
|
|
17104
|
+
con_btn.setMinimumHeight(50)
|
|
17105
|
+
con_btn.clicked.connect(self.start_connectivity)
|
|
17106
|
+
layout.addWidget(con_btn)
|
|
17107
|
+
|
|
17108
|
+
branch_btn = QPushButton("2. Creating 'Branch Networks'")
|
|
17109
|
+
branch_btn.setMinimumHeight(50)
|
|
17110
|
+
branch_btn.clicked.connect(self.start_branch)
|
|
17111
|
+
layout.addWidget(branch_btn)
|
|
17112
|
+
|
|
17113
|
+
prox_btn = QPushButton("3. Creating 'Proximity Networks'")
|
|
17114
|
+
prox_btn.setMinimumHeight(50)
|
|
17115
|
+
prox_btn.clicked.connect(self.start_prox)
|
|
17116
|
+
layout.addWidget(prox_btn)
|
|
17117
|
+
|
|
17118
|
+
analysis_btn = QPushButton("Network and Image Analysis")
|
|
17119
|
+
analysis_btn.setMinimumHeight(50)
|
|
17120
|
+
analysis_btn.clicked.connect(self.start_analysis)
|
|
17121
|
+
layout.addWidget(analysis_btn)
|
|
17122
|
+
|
|
17123
|
+
processing_btn = QPushButton("Image Processing")
|
|
17124
|
+
processing_btn.setMinimumHeight(50)
|
|
17125
|
+
processing_btn.clicked.connect(self.start_process_tutorial)
|
|
17126
|
+
layout.addWidget(processing_btn)
|
|
17127
|
+
|
|
17128
|
+
layout.addStretch()
|
|
17129
|
+
|
|
17130
|
+
# Close button
|
|
17131
|
+
close_btn = QPushButton("Close")
|
|
17132
|
+
close_btn.clicked.connect(self.close)
|
|
17133
|
+
layout.addWidget(close_btn)
|
|
17134
|
+
|
|
17135
|
+
def start_intro(self):
|
|
17136
|
+
"""Start the basic interface tutorial"""
|
|
17137
|
+
self.close()
|
|
17138
|
+
from . import tutorial
|
|
17139
|
+
|
|
17140
|
+
if not hasattr(self.window, 'start_tutorial_manager'):
|
|
17141
|
+
self.window.start_tutorial_manager = tutorial.setup_start_tutorial(self.window)
|
|
17142
|
+
|
|
17143
|
+
self.window.start_tutorial_manager.start()
|
|
17144
|
+
|
|
17145
|
+
def start_basics_tutorial(self):
|
|
17146
|
+
"""Start the basic interface tutorial"""
|
|
17147
|
+
self.close()
|
|
17148
|
+
from . import tutorial
|
|
17149
|
+
|
|
17150
|
+
if not hasattr(self.window, 'basics_tutorial_manager'):
|
|
17151
|
+
self.window.basics_tutorial_manager = tutorial.setup_basics_tutorial(self.window)
|
|
17152
|
+
|
|
17153
|
+
self.window.basics_tutorial_manager.start()
|
|
17154
|
+
|
|
17155
|
+
def start_file(self):
|
|
17156
|
+
"""Start the basic interface tutorial"""
|
|
17157
|
+
self.close()
|
|
17158
|
+
from . import tutorial
|
|
17159
|
+
|
|
17160
|
+
if not hasattr(self.window, 'file_tutorial_manager'):
|
|
17161
|
+
self.window.file_tutorial_manager = tutorial.setup_file_tutorial(self.window)
|
|
17162
|
+
|
|
17163
|
+
self.window.file_tutorial_manager.start()
|
|
17164
|
+
|
|
17165
|
+
|
|
17166
|
+
def start_segment(self):
|
|
17167
|
+
self.close()
|
|
17168
|
+
from . import tutorial
|
|
17169
|
+
|
|
17170
|
+
if not hasattr(self.window, 'seg_tutorial_manager'):
|
|
17171
|
+
self.window.seg_tutorial_manager = tutorial.setup_seg_tutorial(self.window)
|
|
17172
|
+
self.window.seg_tutorial_manager.start()
|
|
17173
|
+
|
|
17174
|
+
def start_connectivity(self):
|
|
17175
|
+
self.close()
|
|
17176
|
+
from . import tutorial
|
|
17177
|
+
|
|
17178
|
+
if not hasattr(self.window, 'connectivity_tutorial_manager'):
|
|
17179
|
+
self.window.connectivity_tutorial_manager = tutorial.setup_connectivity_tutorial(self.window)
|
|
17180
|
+
|
|
17181
|
+
self.window.connectivity_tutorial_manager.start()
|
|
17182
|
+
|
|
17183
|
+
def start_branch(self):
|
|
17184
|
+
self.close()
|
|
17185
|
+
from . import tutorial
|
|
17186
|
+
|
|
17187
|
+
if not hasattr(self.window, 'branch_tutorial_manager'):
|
|
17188
|
+
self.window.branch_tutorial_manager = tutorial.setup_branch_tutorial(self.window)
|
|
17189
|
+
|
|
17190
|
+
self.window.branch_tutorial_manager.start()
|
|
17191
|
+
|
|
17192
|
+
def start_prox(self):
|
|
17193
|
+
self.close()
|
|
17194
|
+
from . import tutorial
|
|
17195
|
+
|
|
17196
|
+
if not hasattr(self.window, 'prox_tutorial_manager'):
|
|
17197
|
+
self.window.prox_tutorial_manager = tutorial.setup_prox_tutorial(self.window)
|
|
17198
|
+
|
|
17199
|
+
self.window.prox_tutorial_manager.start()
|
|
17200
|
+
|
|
17201
|
+
def start_analysis(self):
|
|
17202
|
+
self.close()
|
|
17203
|
+
from . import tutorial
|
|
17204
|
+
|
|
17205
|
+
if not hasattr(self.window, 'analysis_tutorial_manager'):
|
|
17206
|
+
self.window.analysis_tutorial_manager = tutorial.setup_analysis_tutorial(self.window)
|
|
17207
|
+
|
|
17208
|
+
self.window.analysis_tutorial_manager.start()
|
|
17209
|
+
|
|
17210
|
+
def start_process_tutorial(self):
|
|
17211
|
+
"""Start the image processing tutorial"""
|
|
17212
|
+
self.close()
|
|
17213
|
+
from . import tutorial
|
|
17214
|
+
|
|
17215
|
+
if not hasattr(self.window, 'process_tutorial_manager'):
|
|
17216
|
+
self.window.process_tutorial_manager = tutorial.setup_process_tutorial(self.window)
|
|
17217
|
+
|
|
17218
|
+
self.window.process_tutorial_manager.start()
|
|
15965
17219
|
|
|
17220
|
+
def start_image_tutorial(self):
|
|
17221
|
+
"""Start the image tutorial"""
|
|
17222
|
+
self.close()
|
|
17223
|
+
from . import tutorial
|
|
17224
|
+
|
|
17225
|
+
if not hasattr(self.window, 'image_tutorial_manager'):
|
|
17226
|
+
self.window.image_tutorial_manager = tutorial.setup_image_tutorial(self.window)
|
|
17227
|
+
|
|
17228
|
+
self.window.image_tutorial_manager.start()
|
|
15966
17229
|
|
|
15967
17230
|
# Initiating this program from the script line:
|
|
15968
17231
|
|