nettracer3d 0.4.2__py3-none-any.whl → 0.4.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.
- nettracer3d/morphology.py +2 -0
- nettracer3d/nettracer.py +133 -0
- nettracer3d/nettracer_gui.py +472 -97
- nettracer3d/proximity.py +3 -1
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.3.dist-info}/METADATA +3 -9
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.3.dist-info}/RECORD +10 -10
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.3.dist-info}/LICENSE +0 -0
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.3.dist-info}/WHEEL +0 -0
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.3.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.3.dist-info}/top_level.txt +0 -0
nettracer3d/morphology.py
CHANGED
|
@@ -101,6 +101,8 @@ def process_label(args):
|
|
|
101
101
|
nodes, edges, label, dilate_xy, dilate_z, array_shape = args
|
|
102
102
|
print(f"Processing node {label}")
|
|
103
103
|
indices = np.argwhere(nodes == label)
|
|
104
|
+
if len(indices) == 0:
|
|
105
|
+
return None, None, None
|
|
104
106
|
z_vals, y_vals, x_vals = get_reslice_indices((indices, dilate_xy, dilate_z, array_shape))
|
|
105
107
|
if z_vals is None: #If get_reslice_indices ran into a ValueError, nothing is returned.
|
|
106
108
|
return None, None, None
|
nettracer3d/nettracer.py
CHANGED
|
@@ -1003,6 +1003,8 @@ def dilate_3D_recursive(tiff_array, dilated_x, dilated_y, dilated_z, step_size=N
|
|
|
1003
1003
|
max_dilation = max(dilated_x, dilated_y, dilated_z)
|
|
1004
1004
|
if max_dilation < (0.2 * min_dim):
|
|
1005
1005
|
return dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z)
|
|
1006
|
+
elif dilated_x == 1 and dilated_y == 1 and dilated_z == 1: #Also if there is only a single dilation don't do it
|
|
1007
|
+
return dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z)
|
|
1006
1008
|
|
|
1007
1009
|
# Initialize step_size for first call
|
|
1008
1010
|
if step_size is None:
|
|
@@ -1587,6 +1589,51 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
1587
1589
|
|
|
1588
1590
|
return array
|
|
1589
1591
|
|
|
1592
|
+
def fix_branches(array, G, communities, fix_val = None):
|
|
1593
|
+
|
|
1594
|
+
def invert_dict(d):
|
|
1595
|
+
inverted = {}
|
|
1596
|
+
for key, value in d.items():
|
|
1597
|
+
inverted.setdefault(value, []).append(key)
|
|
1598
|
+
return inverted
|
|
1599
|
+
|
|
1600
|
+
def get_degree_threshold(community_degrees):
|
|
1601
|
+
degrees = np.array(community_degrees, dtype=float)
|
|
1602
|
+
hist, bins = np.histogram(degrees, bins='auto')
|
|
1603
|
+
peaks, _ = find_peaks(hist)
|
|
1604
|
+
if len(peaks) > 1:
|
|
1605
|
+
# Get bin value after first peak as threshold
|
|
1606
|
+
return bins[peaks[0] + 1]
|
|
1607
|
+
return 4 # Default fallback
|
|
1608
|
+
|
|
1609
|
+
avg_degree = G.number_of_edges() * 2 / G.number_of_nodes()
|
|
1610
|
+
|
|
1611
|
+
targs = []
|
|
1612
|
+
|
|
1613
|
+
inverted = invert_dict(communities)
|
|
1614
|
+
|
|
1615
|
+
community_degrees = {}
|
|
1616
|
+
|
|
1617
|
+
for com in inverted:
|
|
1618
|
+
subgraph = G.subgraph(inverted[com])
|
|
1619
|
+
sub_degree = subgraph.number_of_edges() * 2/ subgraph.number_of_nodes()
|
|
1620
|
+
community_degrees[com] = sub_degree
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
if fix_val is None:
|
|
1624
|
+
threshold = get_degree_threshold(list(community_degrees.values()))
|
|
1625
|
+
else:
|
|
1626
|
+
threshold = fix_val
|
|
1627
|
+
|
|
1628
|
+
for com in community_degrees:
|
|
1629
|
+
if community_degrees[com] > threshold: #This method of comparison could possibly be more nuanced.
|
|
1630
|
+
targs.append(com)
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
return targs
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
|
|
1590
1637
|
def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = 0, directory = None, return_skele = False, order = 0):
|
|
1591
1638
|
"""
|
|
1592
1639
|
Can be used to label vertices (where multiple branches connect) a binary image. Labelled output will be saved to the active directory if none is specified. Note this works better on already thin filaments and may over-divide larger trunkish objects.
|
|
@@ -3417,6 +3464,92 @@ class Network_3D:
|
|
|
3417
3464
|
self._nodes = self._nodes.astype(np.uint16)
|
|
3418
3465
|
|
|
3419
3466
|
|
|
3467
|
+
def com_to_node(self, targets = None):
|
|
3468
|
+
|
|
3469
|
+
def invert_dict(d):
|
|
3470
|
+
inverted = {}
|
|
3471
|
+
for key, value in d.items():
|
|
3472
|
+
inverted.setdefault(value, []).append(key)
|
|
3473
|
+
return inverted
|
|
3474
|
+
|
|
3475
|
+
def update_array(array_3d, value_dict, targets = None):
|
|
3476
|
+
ref_array = copy.deepcopy(array_3d)
|
|
3477
|
+
if targets is None:
|
|
3478
|
+
for key, value_list in value_dict.items():
|
|
3479
|
+
for value in value_list:
|
|
3480
|
+
array_3d[ref_array == value] = key
|
|
3481
|
+
else:
|
|
3482
|
+
max_val = np.max(array_3d) + 1
|
|
3483
|
+
for key, value_list in value_dict.items():
|
|
3484
|
+
for value in value_list:
|
|
3485
|
+
array_3d[ref_array == value] = max_val
|
|
3486
|
+
max_val += 1
|
|
3487
|
+
|
|
3488
|
+
return array_3d
|
|
3489
|
+
|
|
3490
|
+
if 0 in self.communities.values():
|
|
3491
|
+
self.communities = {k: v + 1 for k, v in self.communities.items()}
|
|
3492
|
+
if targets is not None:
|
|
3493
|
+
for item in targets:
|
|
3494
|
+
item = item + 1
|
|
3495
|
+
|
|
3496
|
+
inverted = invert_dict(self.communities)
|
|
3497
|
+
|
|
3498
|
+
|
|
3499
|
+
if targets is not None:
|
|
3500
|
+
new_inverted = copy.deepcopy(inverted)
|
|
3501
|
+
for com in inverted:
|
|
3502
|
+
if com not in targets:
|
|
3503
|
+
del new_inverted[com]
|
|
3504
|
+
inverted = new_inverted
|
|
3505
|
+
|
|
3506
|
+
|
|
3507
|
+
if self._node_identities is not None:
|
|
3508
|
+
new_identities = {}
|
|
3509
|
+
for com in inverted:
|
|
3510
|
+
new_identities[com] = ""
|
|
3511
|
+
|
|
3512
|
+
list1 = self._network_lists[0] #Get network lists to change
|
|
3513
|
+
list2 = self._network_lists[1]
|
|
3514
|
+
list3 = self._network_lists[2]
|
|
3515
|
+
|
|
3516
|
+
for i in range(len(list1)):
|
|
3517
|
+
list1[i] = self.communities[list1[i]] #Set node at network list spot to its community instead
|
|
3518
|
+
list2[i] = self.communities[list2[i]]
|
|
3519
|
+
if list1[i] == list2[i]: #If the edge corresponding there joins different communities, it will not be set to 0
|
|
3520
|
+
list3[i] = 0
|
|
3521
|
+
|
|
3522
|
+
|
|
3523
|
+
self.network_lists = [list1, list2, list3]
|
|
3524
|
+
|
|
3525
|
+
if self._nodes is not None:
|
|
3526
|
+
self._nodes = update_array(self._nodes, inverted, targets = targets) #Set the array to match the new network
|
|
3527
|
+
|
|
3528
|
+
try:
|
|
3529
|
+
|
|
3530
|
+
if self._node_identities is not None:
|
|
3531
|
+
|
|
3532
|
+
for key, value_list in inverted.items():
|
|
3533
|
+
temp_dict = {}
|
|
3534
|
+
for value in value_list:
|
|
3535
|
+
if self._node_identities[value] in temp_dict:
|
|
3536
|
+
temp_dict[self._node_identities[value]] += 1
|
|
3537
|
+
else:
|
|
3538
|
+
temp_dict[self._node_identities[value]] = 1
|
|
3539
|
+
for id_type, num in temp_dict.items():
|
|
3540
|
+
new_identities[key] += f'ID {id_type}:{num}, '
|
|
3541
|
+
|
|
3542
|
+
self.node_identities = new_identities
|
|
3543
|
+
except:
|
|
3544
|
+
pass
|
|
3545
|
+
|
|
3546
|
+
|
|
3547
|
+
|
|
3548
|
+
|
|
3549
|
+
|
|
3550
|
+
|
|
3551
|
+
|
|
3552
|
+
|
|
3420
3553
|
def trunk_to_node(self):
|
|
3421
3554
|
"""
|
|
3422
3555
|
Converts the edge 'trunk' into a node. In this case, the trunk is the edge that creates the most node-node connections. There may be times when many nodes are connected by a single, expansive edge that obfuscates the rest of the edges. Converting the trunk to a node can better reveal these edges.
|
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -16,6 +16,7 @@ from nettracer3d import nettracer as n3d
|
|
|
16
16
|
from nettracer3d import smart_dilate as sdl
|
|
17
17
|
from nettracer3d import proximity as pxt
|
|
18
18
|
from matplotlib.colors import LinearSegmentedColormap
|
|
19
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
19
20
|
import pandas as pd
|
|
20
21
|
from PyQt6.QtGui import (QFont, QCursor, QColor)
|
|
21
22
|
import tifffile
|
|
@@ -176,6 +177,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
176
177
|
buttons_layout.addWidget(self.pan_button)
|
|
177
178
|
|
|
178
179
|
control_layout.addWidget(buttons_widget)
|
|
180
|
+
|
|
181
|
+
self.preview = False #Whether in preview mode or not
|
|
182
|
+
self.targs = None #Targets for preview mode
|
|
179
183
|
|
|
180
184
|
# Create channel buttons
|
|
181
185
|
self.channel_buttons = []
|
|
@@ -396,7 +400,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
396
400
|
self.slice_slider.setValue(new_value)
|
|
397
401
|
|
|
398
402
|
|
|
399
|
-
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None):
|
|
403
|
+
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
|
|
400
404
|
"""
|
|
401
405
|
Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
|
|
402
406
|
|
|
@@ -410,6 +414,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
410
414
|
mask = np.isin(chunk_data, indices_to_check)
|
|
411
415
|
return mask * 255
|
|
412
416
|
|
|
417
|
+
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
418
|
+
"""Process a single chunk of the array to create highlight mask"""
|
|
419
|
+
|
|
420
|
+
mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
|
|
421
|
+
return mask * 255
|
|
422
|
+
|
|
413
423
|
if node_indices is not None:
|
|
414
424
|
if 0 in node_indices:
|
|
415
425
|
node_indices.remove(0)
|
|
@@ -453,7 +463,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
453
463
|
num_cores = mp.cpu_count()
|
|
454
464
|
|
|
455
465
|
# Calculate chunk size along y-axis
|
|
456
|
-
chunk_size = full_shape[
|
|
466
|
+
chunk_size = full_shape[1] // num_cores
|
|
457
467
|
if chunk_size < 1:
|
|
458
468
|
chunk_size = 1
|
|
459
469
|
|
|
@@ -463,25 +473,32 @@ class ImageViewerWindow(QMainWindow):
|
|
|
463
473
|
|
|
464
474
|
# Create chunks
|
|
465
475
|
chunks = []
|
|
466
|
-
for i in range(0, array_shape[
|
|
467
|
-
end = min(i + chunk_size, array_shape[
|
|
468
|
-
chunks.append(channel_data[i:end])
|
|
476
|
+
for i in range(0, array_shape[1], chunk_size):
|
|
477
|
+
end = min(i + chunk_size, array_shape[1])
|
|
478
|
+
chunks.append(channel_data[:, i:end, :])
|
|
469
479
|
|
|
470
480
|
# Process chunks in parallel using ThreadPoolExecutor
|
|
471
|
-
|
|
481
|
+
if not bounds:
|
|
482
|
+
process_func = partial(process_chunk, indices_to_check=indices)
|
|
483
|
+
else:
|
|
484
|
+
if len(indices) == 1:
|
|
485
|
+
indices.insert(0, 0)
|
|
486
|
+
process_func = partial(process_chunk_bounds, indices_to_check=indices)
|
|
487
|
+
|
|
472
488
|
|
|
473
489
|
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
474
490
|
chunk_results = list(executor.map(process_func, chunks))
|
|
475
491
|
|
|
476
492
|
# Reassemble the chunks
|
|
477
|
-
return np.
|
|
493
|
+
return np.concatenate(chunk_results, axis=1)
|
|
478
494
|
|
|
479
495
|
# Process nodes and edges in parallel using multiprocessing
|
|
480
|
-
with ThreadPoolExecutor(max_workers=
|
|
496
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
481
497
|
future_nodes = executor.submit(process_channel, self.channel_data[0], node_indices, full_shape)
|
|
482
498
|
future_edges = executor.submit(process_channel, self.channel_data[1], edge_indices, full_shape)
|
|
483
499
|
future_overlay1 = executor.submit(process_channel, self.channel_data[2], overlay1_indices, full_shape)
|
|
484
500
|
future_overlay2 = executor.submit(process_channel, self.channel_data[3], overlay2_indices, full_shape)
|
|
501
|
+
|
|
485
502
|
|
|
486
503
|
# Get results
|
|
487
504
|
node_overlay = future_nodes.result()
|
|
@@ -502,6 +519,84 @@ class ImageViewerWindow(QMainWindow):
|
|
|
502
519
|
# Update display
|
|
503
520
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
504
521
|
|
|
522
|
+
def create_highlight_overlay_slice(self, indices, bounds = False):
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
526
|
+
"""Process a single chunk of the array to create highlight mask"""
|
|
527
|
+
mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
|
|
528
|
+
return mask * 255
|
|
529
|
+
|
|
530
|
+
def process_chunk(chunk_data, indices_to_check):
|
|
531
|
+
"""Process a single chunk of the array to create highlight mask"""
|
|
532
|
+
|
|
533
|
+
mask = np.isin(chunk_data, indices_to_check)
|
|
534
|
+
return mask * 255
|
|
535
|
+
|
|
536
|
+
array = self.channel_data[self.active_channel]
|
|
537
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
538
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
539
|
+
|
|
540
|
+
current_slice = array[self.current_slice, :, :]
|
|
541
|
+
full_shape = array.shape
|
|
542
|
+
slice_shape = current_slice.shape
|
|
543
|
+
|
|
544
|
+
if self.highlight_overlay is None:
|
|
545
|
+
|
|
546
|
+
self.highlight_overlay = np.zeros(full_shape, dtype=np.uint8)
|
|
547
|
+
|
|
548
|
+
# Get number of CPU cores
|
|
549
|
+
num_cores = mp.cpu_count()
|
|
550
|
+
|
|
551
|
+
# Calculate chunk size along y-axis
|
|
552
|
+
chunk_size = slice_shape[0] // num_cores
|
|
553
|
+
if chunk_size < 1:
|
|
554
|
+
chunk_size = 1
|
|
555
|
+
|
|
556
|
+
def process_channel(channel_data, indices, array_shape):
|
|
557
|
+
if channel_data is None or not indices:
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
# Create chunks
|
|
561
|
+
chunks = []
|
|
562
|
+
for i in range(0, array_shape[0], chunk_size):
|
|
563
|
+
end = min(i + chunk_size, array_shape[0])
|
|
564
|
+
chunks.append(channel_data[i:end])
|
|
565
|
+
|
|
566
|
+
# Process chunks in parallel using ThreadPoolExecutor
|
|
567
|
+
if not bounds:
|
|
568
|
+
process_func = partial(process_chunk, indices_to_check=indices)
|
|
569
|
+
else:
|
|
570
|
+
if len(indices) == 1:
|
|
571
|
+
indices.insert(0, 0)
|
|
572
|
+
process_func = partial(process_chunk_bounds, indices_to_check=indices)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
576
|
+
chunk_results = list(executor.map(process_func, chunks))
|
|
577
|
+
|
|
578
|
+
# Reassemble the chunks
|
|
579
|
+
return np.vstack(chunk_results)
|
|
580
|
+
|
|
581
|
+
# Process nodes and edges in parallel using multiprocessing
|
|
582
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
583
|
+
future_highlight = executor.submit(process_channel, current_slice, indices, slice_shape)
|
|
584
|
+
|
|
585
|
+
# Get results
|
|
586
|
+
overlay = future_highlight.result()
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
|
|
590
|
+
self.highlight_overlay[self.current_slice, :, :] = overlay
|
|
591
|
+
except:
|
|
592
|
+
pass
|
|
593
|
+
|
|
594
|
+
# Update display
|
|
595
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), called = True)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
505
600
|
|
|
506
601
|
|
|
507
602
|
|
|
@@ -1529,6 +1624,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1529
1624
|
self.selection_rect = None
|
|
1530
1625
|
self.canvas.draw()
|
|
1531
1626
|
|
|
1627
|
+
|
|
1532
1628
|
def highlight_value_in_tables(self, clicked_value):
|
|
1533
1629
|
"""Helper method to find and highlight a value in both tables."""
|
|
1534
1630
|
|
|
@@ -2055,8 +2151,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2055
2151
|
|
|
2056
2152
|
def show_thresh_dialog(self):
|
|
2057
2153
|
"""Show threshold dialog"""
|
|
2058
|
-
|
|
2059
|
-
|
|
2154
|
+
dialog = ThresholdDialog(self)
|
|
2155
|
+
dialog.exec()
|
|
2156
|
+
|
|
2060
2157
|
|
|
2061
2158
|
def show_mask_dialog(self):
|
|
2062
2159
|
"""Show the mask dialog"""
|
|
@@ -2265,13 +2362,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2265
2362
|
|
|
2266
2363
|
|
|
2267
2364
|
except Exception as e:
|
|
2268
|
-
import traceback
|
|
2269
|
-
print(traceback.format_exc())
|
|
2270
2365
|
print(f"An error has occured: {e}")
|
|
2271
2366
|
|
|
2272
2367
|
except Exception as e:
|
|
2273
|
-
|
|
2274
|
-
print(traceback.format_exc())
|
|
2368
|
+
|
|
2275
2369
|
QMessageBox.critical(
|
|
2276
2370
|
self,
|
|
2277
2371
|
"Error Loading",
|
|
@@ -2513,12 +2607,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2513
2607
|
"""Shows a dialog asking user to confirm if image is 2D RGB"""
|
|
2514
2608
|
msg = QMessageBox()
|
|
2515
2609
|
msg.setIcon(QMessageBox.Icon.Question)
|
|
2516
|
-
msg.setText("Image Format
|
|
2610
|
+
msg.setText("Image Format Alert")
|
|
2517
2611
|
msg.setInformativeText("Is this a 2D color (RGB/CMYK) image?")
|
|
2518
2612
|
msg.setWindowTitle("Confirm Image Format")
|
|
2519
2613
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
2520
2614
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
2521
2615
|
|
|
2616
|
+
def confirm_resize_dialog(self):
|
|
2617
|
+
"""Shows a dialog asking user to resize image"""
|
|
2618
|
+
msg = QMessageBox()
|
|
2619
|
+
msg.setIcon(QMessageBox.Icon.Question)
|
|
2620
|
+
msg.setText("Image Format Alert")
|
|
2621
|
+
msg.setInformativeText(f"This image is a different shape than the ones loaded into the viewer window. Trying to run processes with images of different sizes has a high probability of crashing the program.\nPress yes to resize the new image to the other images. Press no to load it anyway.")
|
|
2622
|
+
msg.setWindowTitle("Resize")
|
|
2623
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
2624
|
+
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
2625
|
+
|
|
2522
2626
|
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
|
|
2523
2627
|
"""Load a channel and enable active channel selection if needed."""
|
|
2524
2628
|
|
|
@@ -2547,6 +2651,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2547
2651
|
if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
|
|
2548
2652
|
self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index])
|
|
2549
2653
|
|
|
2654
|
+
for i in range(4): #Try to ensure users don't load in different sized arrays
|
|
2655
|
+
if self.channel_data[i] is None or i == channel_index or data:
|
|
2656
|
+
if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
|
|
2657
|
+
if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
|
|
2658
|
+
self.highlight_overlay = None
|
|
2659
|
+
continue
|
|
2660
|
+
else:
|
|
2661
|
+
old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
|
|
2662
|
+
if old_shape != self.channel_data[channel_index].shape[:3]:
|
|
2663
|
+
if self.confirm_resize_dialog():
|
|
2664
|
+
self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
|
|
2665
|
+
break
|
|
2666
|
+
|
|
2550
2667
|
|
|
2551
2668
|
if channel_index == 0:
|
|
2552
2669
|
my_network.nodes = self.channel_data[channel_index]
|
|
@@ -2601,7 +2718,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2601
2718
|
self.volume_dict[channel_index] = None #reset volumes
|
|
2602
2719
|
|
|
2603
2720
|
if assign_shape: #keep original shape tracked to undo resampling.
|
|
2604
|
-
self.original_shape
|
|
2721
|
+
if self.original_shape is None:
|
|
2722
|
+
self.original_shape = self.channel_data[channel_index].shape
|
|
2723
|
+
elif self.original_shape[0] < self.channel_data[channel_index].shape[0] or self.original_shape[1] < self.channel_data[channel_index].shape[1] or self.original_shape[2] < self.channel_data[channel_index].shape[2]:
|
|
2724
|
+
self.original_shape = self.channel_data[channel_index].shape
|
|
2605
2725
|
if len(self.original_shape) == 4:
|
|
2606
2726
|
self.original_shape = (self.original_shape[0], self.original_shape[1], self.original_shape[2])
|
|
2607
2727
|
|
|
@@ -2829,11 +2949,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2829
2949
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2830
2950
|
# Convert slider values (0-100) to data values (0-1)
|
|
2831
2951
|
min_val, max_val = values
|
|
2832
|
-
self.channel_brightness[channel_index]['min'] = min_val / 255
|
|
2952
|
+
self.channel_brightness[channel_index]['min'] = min_val / 255 #Accomodate 32 bit data?
|
|
2833
2953
|
self.channel_brightness[channel_index]['max'] = max_val / 255
|
|
2834
2954
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2835
2955
|
|
|
2836
|
-
def update_display(self, preserve_zoom=None, dims = None):
|
|
2956
|
+
def update_display(self, preserve_zoom=None, dims = None, called = False):
|
|
2837
2957
|
"""Update the display with currently visible channels and highlight overlay."""
|
|
2838
2958
|
|
|
2839
2959
|
self.figure.clear()
|
|
@@ -2927,6 +3047,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2927
3047
|
vmax=1,
|
|
2928
3048
|
extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
|
|
2929
3049
|
|
|
3050
|
+
if self.preview and not called:
|
|
3051
|
+
self.create_highlight_overlay_slice(self.targs, bounds = self.bounds)
|
|
3052
|
+
|
|
2930
3053
|
# Add highlight overlay if it exists
|
|
2931
3054
|
if self.highlight_overlay is not None:
|
|
2932
3055
|
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
@@ -2939,6 +3062,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2939
3062
|
alpha=0.5)
|
|
2940
3063
|
|
|
2941
3064
|
|
|
3065
|
+
|
|
2942
3066
|
|
|
2943
3067
|
# Style the axes
|
|
2944
3068
|
self.ax.set_xlabel('X')
|
|
@@ -4162,17 +4286,18 @@ class WhiteDialog(QDialog):
|
|
|
4162
4286
|
def white_overlay(self):
|
|
4163
4287
|
|
|
4164
4288
|
try:
|
|
4165
|
-
|
|
4166
|
-
try:
|
|
4289
|
+
if isinstance(my_network.nodes, np.ndarray) :
|
|
4167
4290
|
overlay = np.ones_like(my_network.nodes).astype(np.uint8) * 255
|
|
4168
|
-
|
|
4291
|
+
elif isinstance(my_network.edges, np.ndarray):
|
|
4169
4292
|
overlay = np.ones_like(my_network.edges).astype(np.uint8) * 255
|
|
4170
|
-
|
|
4171
|
-
my_network.
|
|
4293
|
+
elif isinstance(my_network.network_overlay, np.ndarray):
|
|
4294
|
+
overlay = np.ones_like(my_network.network_overlay).astype(np.uint8) * 255
|
|
4172
4295
|
|
|
4173
|
-
|
|
4296
|
+
my_network.id_overlay = overlay
|
|
4174
4297
|
|
|
4175
|
-
|
|
4298
|
+
self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
|
|
4299
|
+
|
|
4300
|
+
self.accept()
|
|
4176
4301
|
|
|
4177
4302
|
except Exception as e:
|
|
4178
4303
|
print(f"Error making white background: {e}")
|
|
@@ -4274,7 +4399,7 @@ class NetShowDialog(QDialog):
|
|
|
4274
4399
|
self.geo_layout = QPushButton("geo_layout")
|
|
4275
4400
|
self.geo_layout.setCheckable(True)
|
|
4276
4401
|
self.geo_layout.setChecked(False)
|
|
4277
|
-
layout.addRow("Use
|
|
4402
|
+
layout.addRow("Use Geographic Layout:", self.geo_layout)
|
|
4278
4403
|
|
|
4279
4404
|
# Add mode selection dropdown
|
|
4280
4405
|
self.mode_selector = QComboBox()
|
|
@@ -5246,101 +5371,303 @@ class LabelDialog(QDialog):
|
|
|
5246
5371
|
f"Error running label: {str(e)}"
|
|
5247
5372
|
)
|
|
5248
5373
|
|
|
5249
|
-
class
|
|
5374
|
+
class ThresholdDialog(QDialog):
|
|
5250
5375
|
def __init__(self, parent=None):
|
|
5251
5376
|
super().__init__(parent)
|
|
5252
|
-
self.setWindowTitle("Threshold
|
|
5253
|
-
|
|
5254
|
-
# Create central widget and layout
|
|
5255
|
-
central_widget = QWidget()
|
|
5256
|
-
self.setCentralWidget(central_widget)
|
|
5257
|
-
layout = QFormLayout(central_widget)
|
|
5258
|
-
|
|
5259
|
-
self.min = QLineEdit("")
|
|
5260
|
-
layout.addRow("Minimum Value to retain:", self.min)
|
|
5377
|
+
self.setWindowTitle("Choose Threshold Mode")
|
|
5378
|
+
self.setModal(True)
|
|
5261
5379
|
|
|
5262
|
-
|
|
5263
|
-
self.max = QLineEdit("")
|
|
5264
|
-
layout.addRow("Maximum Value to retain:", self.max)
|
|
5380
|
+
layout = QFormLayout(self)
|
|
5265
5381
|
|
|
5266
5382
|
# Add mode selection dropdown
|
|
5267
5383
|
self.mode_selector = QComboBox()
|
|
5268
|
-
self.mode_selector.addItems(["Using
|
|
5384
|
+
self.mode_selector.addItems(["Using Label/Brightness", "Using Volumes"])
|
|
5269
5385
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
5270
5386
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
5271
5387
|
|
|
5272
5388
|
# Add Run button
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
layout.addRow(
|
|
5389
|
+
run_button = QPushButton("Select")
|
|
5390
|
+
run_button.clicked.connect(self.thresh_mode)
|
|
5391
|
+
layout.addRow(run_button)
|
|
5392
|
+
|
|
5393
|
+
def thresh_mode(self):
|
|
5394
|
+
|
|
5395
|
+
try:
|
|
5396
|
+
|
|
5397
|
+
accepted_mode = self.mode_selector.currentIndex()
|
|
5398
|
+
|
|
5399
|
+
if accepted_mode == 1:
|
|
5400
|
+
if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
|
|
5401
|
+
self.parent().show_label_dialog()
|
|
5402
|
+
|
|
5403
|
+
if self.parent().volume_dict[self.parent().active_channel] is None:
|
|
5404
|
+
self.parent().volumes()
|
|
5405
|
+
|
|
5406
|
+
thresh_window = ThresholdWindow(self.parent(), accepted_mode)
|
|
5407
|
+
thresh_window.show() # Non-modal window
|
|
5408
|
+
self.highlight_overlay = None
|
|
5409
|
+
self.accept()
|
|
5410
|
+
except:
|
|
5411
|
+
pass
|
|
5412
|
+
|
|
5413
|
+
|
|
5414
|
+
|
|
5415
|
+
|
|
5416
|
+
|
|
5417
|
+
class ThresholdWindow(QMainWindow):
|
|
5418
|
+
def __init__(self, parent=None, accepted_mode=0):
|
|
5419
|
+
super().__init__(parent)
|
|
5420
|
+
self.setWindowTitle("Threshold")
|
|
5276
5421
|
|
|
5277
|
-
#
|
|
5422
|
+
# Create central widget and layout
|
|
5423
|
+
central_widget = QWidget()
|
|
5424
|
+
self.setCentralWidget(central_widget)
|
|
5425
|
+
layout = QVBoxLayout(central_widget)
|
|
5426
|
+
|
|
5427
|
+
# Get histogram data
|
|
5428
|
+
if accepted_mode == 1:
|
|
5429
|
+
self.histo_list = list(self.parent().volume_dict[self.parent().active_channel].values())
|
|
5430
|
+
self.bounds = False
|
|
5431
|
+
self.parent().bounds = False
|
|
5432
|
+
elif accepted_mode == 0:
|
|
5433
|
+
self.histo_list = self.parent().channel_data[self.parent().active_channel].flatten().tolist()
|
|
5434
|
+
self.bounds = True
|
|
5435
|
+
self.parent().bounds = True
|
|
5436
|
+
|
|
5437
|
+
# Create matplotlib figure
|
|
5438
|
+
fig = Figure(figsize=(5, 4))
|
|
5439
|
+
self.canvas = FigureCanvas(fig)
|
|
5440
|
+
layout.addWidget(self.canvas)
|
|
5441
|
+
|
|
5442
|
+
# Pre-compute histogram with numpy
|
|
5443
|
+
counts, bin_edges = np.histogram(self.histo_list, bins=50)
|
|
5444
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
5445
|
+
|
|
5446
|
+
# Plot pre-computed histogram
|
|
5447
|
+
self.ax = fig.add_subplot(111)
|
|
5448
|
+
self.ax.bar(bin_centers, counts, width=bin_edges[1] - bin_edges[0], alpha=0.5)
|
|
5449
|
+
|
|
5450
|
+
# Add vertical lines for thresholds
|
|
5451
|
+
self.min_line = self.ax.axvline(min(self.histo_list), color='r')
|
|
5452
|
+
self.max_line = self.ax.axvline(max(self.histo_list), color='b')
|
|
5453
|
+
|
|
5454
|
+
# Connect events for dragging
|
|
5455
|
+
self.canvas.mpl_connect('button_press_event', self.on_press)
|
|
5456
|
+
self.canvas.mpl_connect('motion_notify_event', self.on_motion)
|
|
5457
|
+
self.canvas.mpl_connect('button_release_event', self.on_release)
|
|
5458
|
+
|
|
5459
|
+
self.dragging = None
|
|
5460
|
+
|
|
5461
|
+
# Store histogram bounds
|
|
5462
|
+
if self.bounds:
|
|
5463
|
+
self.data_min = 0
|
|
5464
|
+
else:
|
|
5465
|
+
self.data_min = min(self.histo_list)
|
|
5466
|
+
self.data_max = max(self.histo_list)
|
|
5467
|
+
|
|
5468
|
+
# Create form layout for inputs
|
|
5469
|
+
form_layout = QFormLayout()
|
|
5470
|
+
|
|
5471
|
+
self.min = QLineEdit(f"{self.data_min}")
|
|
5472
|
+
self.min.editingFinished.connect(self.min_value_changed)
|
|
5473
|
+
form_layout.addRow("Minimum Value to retain:", self.min)
|
|
5474
|
+
self.prev_min = self.data_min
|
|
5475
|
+
|
|
5476
|
+
self.max = QLineEdit(f"{self.data_max}")
|
|
5477
|
+
self.max.editingFinished.connect(self.max_value_changed)
|
|
5478
|
+
form_layout.addRow("Maximum Value to retain:", self.max)
|
|
5479
|
+
self.prev_max = self.data_max
|
|
5480
|
+
|
|
5481
|
+
self.targs = [self.prev_min, self.prev_max]
|
|
5482
|
+
|
|
5483
|
+
# preview checkbox (default False)
|
|
5484
|
+
self.preview = QPushButton("Preview")
|
|
5485
|
+
self.preview.setCheckable(True)
|
|
5486
|
+
self.preview.setChecked(False)
|
|
5487
|
+
self.preview.clicked.connect(self.preview_mode)
|
|
5488
|
+
form_layout.addRow("Show Preview:", self.preview)
|
|
5489
|
+
|
|
5278
5490
|
run_button = QPushButton("Apply Threshold")
|
|
5279
5491
|
run_button.clicked.connect(self.thresh)
|
|
5280
|
-
|
|
5492
|
+
form_layout.addRow(run_button)
|
|
5281
5493
|
|
|
5282
|
-
|
|
5283
|
-
self.setMinimumWidth(300)
|
|
5494
|
+
layout.addLayout(form_layout)
|
|
5284
5495
|
|
|
5285
|
-
|
|
5496
|
+
# Set a reasonable default size
|
|
5497
|
+
self.setMinimumWidth(400)
|
|
5498
|
+
self.setMinimumHeight(400)
|
|
5286
5499
|
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
print(f"Invalid input: {text}")
|
|
5292
|
-
return default_value
|
|
5500
|
+
def closeEvent(self, event):
|
|
5501
|
+
self.parent().preview = False
|
|
5502
|
+
self.parent().targs = None
|
|
5503
|
+
self.parent().bounds = False
|
|
5293
5504
|
|
|
5505
|
+
def get_values_in_range(self, lst, min_val, max_val):
|
|
5506
|
+
values = [x for x in lst if min_val <= x <= max_val]
|
|
5507
|
+
output = []
|
|
5508
|
+
for item in self.parent().volume_dict[self.parent().active_channel]:
|
|
5509
|
+
if self.parent().volume_dict[self.parent().active_channel][item] in values:
|
|
5510
|
+
output.append(item)
|
|
5511
|
+
return output
|
|
5512
|
+
|
|
5513
|
+
|
|
5514
|
+
def min_value_changed(self):
|
|
5294
5515
|
try:
|
|
5295
|
-
|
|
5296
|
-
|
|
5516
|
+
text = self.min.text()
|
|
5517
|
+
if not text: # If empty, ignore
|
|
5518
|
+
return
|
|
5297
5519
|
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5520
|
+
try:
|
|
5521
|
+
value = float(text)
|
|
5522
|
+
|
|
5523
|
+
# Bound check against data limits
|
|
5524
|
+
value = max(self.data_min, value)
|
|
5525
|
+
|
|
5526
|
+
# Check against max line
|
|
5527
|
+
max_val = float(self.max.text()) if self.max.text() else self.data_max
|
|
5528
|
+
if value > max_val:
|
|
5529
|
+
# If min would exceed max, set max to its highest possible value
|
|
5530
|
+
self.max.setText(str(round(self.data_max, 2)))
|
|
5531
|
+
self.max_line.set_xdata([self.data_max, self.data_max])
|
|
5532
|
+
# And set min to the previous max value
|
|
5533
|
+
value = max_val
|
|
5534
|
+
self.min.setText(str(round(value, 2)))
|
|
5535
|
+
|
|
5536
|
+
if value == self.prev_min:
|
|
5537
|
+
return
|
|
5538
|
+
else:
|
|
5539
|
+
self.prev_min = value
|
|
5540
|
+
if self.bounds:
|
|
5541
|
+
self.targs = [self.prev_min, self.prev_max]
|
|
5542
|
+
else:
|
|
5543
|
+
self.targs = self.get_values_in_range(self.histo_list, self.prev_min, self.prev_max)
|
|
5544
|
+
self.parent().targs = self.targs
|
|
5545
|
+
if self.preview.isChecked():
|
|
5546
|
+
self.parent().highlight_overlay = None
|
|
5547
|
+
self.parent().create_highlight_overlay_slice(self.targs, bounds = self.bounds)
|
|
5548
|
+
|
|
5549
|
+
# Update the line
|
|
5550
|
+
self.min_line.set_xdata([value, value])
|
|
5551
|
+
self.canvas.draw()
|
|
5552
|
+
|
|
5301
5553
|
|
|
5302
|
-
if self.parent().volume_dict[channel] is None:
|
|
5303
|
-
self.parent().volumes()
|
|
5304
|
-
|
|
5305
|
-
volumes = self.parent().volume_dict[channel]
|
|
5306
|
-
default_max = max(volumes.values())
|
|
5307
|
-
default_min = min(volumes.values())
|
|
5308
5554
|
|
|
5309
|
-
|
|
5310
|
-
|
|
5555
|
+
except ValueError:
|
|
5556
|
+
# If invalid number, reset to current line position
|
|
5557
|
+
self.min.setText(str(round(self.min_line.get_xself.data_mindata()[0], 2)))
|
|
5558
|
+
except:
|
|
5559
|
+
pass
|
|
5560
|
+
|
|
5561
|
+
def max_value_changed(self):
|
|
5562
|
+
try:
|
|
5563
|
+
text = self.max.text()
|
|
5564
|
+
if not text: # If empty, ignore
|
|
5565
|
+
return
|
|
5311
5566
|
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
elif accepted_mode == 1:
|
|
5316
|
-
channel_data = self.parent().channel_data[self.parent().active_channel]
|
|
5317
|
-
default_max = np.max(channel_data)
|
|
5318
|
-
default_min = np.min(channel_data)
|
|
5567
|
+
try:
|
|
5568
|
+
value = float(text)
|
|
5319
5569
|
|
|
5320
|
-
|
|
5321
|
-
|
|
5570
|
+
# Bound check against data limits
|
|
5571
|
+
value = min(self.data_max, value)
|
|
5322
5572
|
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5573
|
+
# Check against min line
|
|
5574
|
+
min_val = float(self.min.text()) if self.min.text() else self.data_min
|
|
5575
|
+
if value < min_val:
|
|
5576
|
+
# If max would go below min, set min to its lowest possible value
|
|
5577
|
+
self.min.setText(str(round(self.data_min, 2)))
|
|
5578
|
+
self.min_line.set_xdata([self.data_min, self.data_min])
|
|
5579
|
+
# And set max to the previous min value
|
|
5580
|
+
value = min_val
|
|
5581
|
+
self.max.setText(str(round(value, 2)))
|
|
5582
|
+
|
|
5583
|
+
if value == self.prev_max:
|
|
5584
|
+
return
|
|
5585
|
+
else:
|
|
5586
|
+
self.prev_max = value
|
|
5587
|
+
if self.bounds:
|
|
5588
|
+
self.targs = [self.prev_min, self.prev_max]
|
|
5589
|
+
else:
|
|
5590
|
+
self.targs = self.get_values_in_range(self.histo_list, self.prev_min, self.prev_max)
|
|
5591
|
+
self.parent().targs = self.targs
|
|
5592
|
+
if self.preview.isChecked():
|
|
5593
|
+
self.parent().highlight_overlay = None
|
|
5594
|
+
self.parent().create_highlight_overlay_slice(self.targs, bounds = self.bounds)
|
|
5595
|
+
|
|
5596
|
+
# Update the line
|
|
5597
|
+
self.max_line.set_xdata([value, value])
|
|
5598
|
+
self.canvas.draw()
|
|
5599
|
+
|
|
5600
|
+
|
|
5601
|
+
|
|
5602
|
+
|
|
5603
|
+
|
|
5604
|
+
|
|
5605
|
+
except ValueError:
|
|
5606
|
+
# If invalid number, reset to current line position
|
|
5607
|
+
self.max.setText(str(round(self.max_line.get_xdata()[0], 2)))
|
|
5608
|
+
except:
|
|
5609
|
+
pass
|
|
5610
|
+
|
|
5611
|
+
def on_press(self, event):
|
|
5612
|
+
try:
|
|
5613
|
+
if event.inaxes != self.ax:
|
|
5614
|
+
return
|
|
5327
5615
|
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
elif
|
|
5333
|
-
self.
|
|
5334
|
-
|
|
5335
|
-
|
|
5616
|
+
# Left click controls left line
|
|
5617
|
+
if event.button == 1: # Left click
|
|
5618
|
+
self.dragging = 'min'
|
|
5619
|
+
# Right click controls right line
|
|
5620
|
+
elif event.button == 3: # Right click
|
|
5621
|
+
self.dragging = 'max'
|
|
5622
|
+
except:
|
|
5623
|
+
pass
|
|
5624
|
+
|
|
5625
|
+
def on_motion(self, event):
|
|
5626
|
+
try:
|
|
5627
|
+
if not self.dragging or event.inaxes != self.ax:
|
|
5628
|
+
return
|
|
5629
|
+
|
|
5630
|
+
if self.dragging == 'min':
|
|
5631
|
+
if event.xdata < self.max_line.get_xdata()[0]:
|
|
5632
|
+
self.min_line.set_xdata([event.xdata, event.xdata])
|
|
5633
|
+
self.min.setText(str(round(event.xdata, 2)))
|
|
5634
|
+
else:
|
|
5635
|
+
if event.xdata > self.min_line.get_xdata()[0]:
|
|
5636
|
+
self.max_line.set_xdata([event.xdata, event.xdata])
|
|
5637
|
+
self.max.setText(str(round(event.xdata, 2)))
|
|
5638
|
+
|
|
5639
|
+
self.canvas.draw()
|
|
5640
|
+
except:
|
|
5641
|
+
pass
|
|
5642
|
+
|
|
5643
|
+
def on_release(self, event):
|
|
5644
|
+
self.min_value_changed()
|
|
5645
|
+
self.max_value_changed()
|
|
5646
|
+
self.dragging = None
|
|
5336
5647
|
|
|
5337
|
-
|
|
5338
|
-
|
|
5648
|
+
def preview_mode(self):
|
|
5649
|
+
try:
|
|
5650
|
+
preview = self.preview.isChecked()
|
|
5651
|
+
self.parent().preview = preview
|
|
5652
|
+
self.parent().targs = self.targs
|
|
5653
|
+
|
|
5654
|
+
if preview and self.targs is not None:
|
|
5655
|
+
self.parent().create_highlight_overlay_slice(self.parent().targs, bounds = self.bounds)
|
|
5656
|
+
except:
|
|
5657
|
+
pass
|
|
5339
5658
|
|
|
5340
5659
|
def thresh(self):
|
|
5341
5660
|
try:
|
|
5342
5661
|
|
|
5343
|
-
self.
|
|
5662
|
+
if self.parent().active_channel == 0:
|
|
5663
|
+
self.parent().create_highlight_overlay(node_indices = self.targs, bounds = self.bounds)
|
|
5664
|
+
elif self.parent().active_channel == 1:
|
|
5665
|
+
self.parent().create_highlight_overlay(edge_indices = self.targs, bounds = self.bounds)
|
|
5666
|
+
elif self.parent().active_channel == 2:
|
|
5667
|
+
self.parent().create_highlight_overlay(overlay1_indices = self.targs, bounds = self.bounds)
|
|
5668
|
+
elif self.parent().active_channel == 3:
|
|
5669
|
+
self.parent().create_highlight_overlay(overlay2_indices = self.targs, bounds = self.bounds)
|
|
5670
|
+
|
|
5344
5671
|
channel_data = self.parent().channel_data[self.parent().active_channel]
|
|
5345
5672
|
mask = self.parent().highlight_overlay > 0
|
|
5346
5673
|
channel_data = channel_data * mask
|
|
@@ -5349,6 +5676,8 @@ class ThresholdWindow(QMainWindow):
|
|
|
5349
5676
|
self.close()
|
|
5350
5677
|
|
|
5351
5678
|
except Exception as e:
|
|
5679
|
+
import traceback
|
|
5680
|
+
print(traceback.format_exc())
|
|
5352
5681
|
QMessageBox.critical(
|
|
5353
5682
|
self,
|
|
5354
5683
|
"Error",
|
|
@@ -6119,11 +6448,21 @@ class BranchDialog(QDialog):
|
|
|
6119
6448
|
self.nodes.setChecked(True)
|
|
6120
6449
|
layout.addRow("Generate nodes from edges? (Skip if already completed - presumes your edge skeleton from generate nodes is in Edges and that your original Edges are in Overlay 2):", self.nodes)
|
|
6121
6450
|
|
|
6122
|
-
# GPU checkbox (default
|
|
6451
|
+
# GPU checkbox (default False)
|
|
6123
6452
|
self.GPU = QPushButton("GPU")
|
|
6124
6453
|
self.GPU.setCheckable(True)
|
|
6125
6454
|
self.GPU.setChecked(False)
|
|
6126
|
-
layout.addRow("Use GPU (Note this may need to temporarily downsample your large images which may simplify outputs - Only memory errors but not permission errors for accessing
|
|
6455
|
+
layout.addRow("Use GPU (Note this may need to temporarily downsample your large images which may simplify outputs - Only memory errors but not permission errors for accessing VRAM are handled by default - CPU will never try to downsample):", self.GPU)
|
|
6456
|
+
|
|
6457
|
+
# Branch Fix checkbox (default False)
|
|
6458
|
+
self.fix = QPushButton("Auto-Correct Branches")
|
|
6459
|
+
self.fix.setCheckable(True)
|
|
6460
|
+
self.fix.setChecked(False)
|
|
6461
|
+
layout.addRow("Attempt to auto-correct branch labels:", self.fix)
|
|
6462
|
+
|
|
6463
|
+
self.fix_val = QLineEdit()
|
|
6464
|
+
self.fix_val.setPlaceholderText("Empty = default value...")
|
|
6465
|
+
layout.addRow("If checked above - Avg Degree of Nearby Branch Communities to Merge (Attempt to fix branch labeling - try 4 to 6 to start or leave empty):", self.fix_val)
|
|
6127
6466
|
|
|
6128
6467
|
self.down_factor = QLineEdit("0")
|
|
6129
6468
|
layout.addRow("Internal downsample (will have to recompute nodes)?:", self.down_factor)
|
|
@@ -6151,6 +6490,9 @@ class BranchDialog(QDialog):
|
|
|
6151
6490
|
nodes = self.nodes.isChecked()
|
|
6152
6491
|
GPU = self.GPU.isChecked()
|
|
6153
6492
|
cubic = self.cubic.isChecked()
|
|
6493
|
+
fix = self.fix.isChecked()
|
|
6494
|
+
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
6495
|
+
|
|
6154
6496
|
|
|
6155
6497
|
|
|
6156
6498
|
original_shape = my_network.edges.shape
|
|
@@ -6158,7 +6500,7 @@ class BranchDialog(QDialog):
|
|
|
6158
6500
|
|
|
6159
6501
|
if down_factor > 0:
|
|
6160
6502
|
self.parent().show_gennodes_dialog(down_factor = [down_factor, cubic], called = True)
|
|
6161
|
-
elif nodes:
|
|
6503
|
+
elif nodes or my_network.nodes is None:
|
|
6162
6504
|
self.parent().show_gennodes_dialog(called = True)
|
|
6163
6505
|
down_factor = None
|
|
6164
6506
|
|
|
@@ -6166,6 +6508,21 @@ class BranchDialog(QDialog):
|
|
|
6166
6508
|
|
|
6167
6509
|
output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
|
|
6168
6510
|
|
|
6511
|
+
if fix:
|
|
6512
|
+
|
|
6513
|
+
temp_network = n3d.Network_3D(nodes = output)
|
|
6514
|
+
|
|
6515
|
+
temp_network.morph_proximity(search = 1) #Detect network of nearby branches
|
|
6516
|
+
|
|
6517
|
+
temp_network.community_partition(weighted = False, style = 1, dostats = False) #Find communities with louvain, unweighted params
|
|
6518
|
+
|
|
6519
|
+
targs = n3d.fix_branches(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
|
|
6520
|
+
|
|
6521
|
+
temp_network.com_to_node(targs)
|
|
6522
|
+
|
|
6523
|
+
output = temp_network.nodes
|
|
6524
|
+
|
|
6525
|
+
|
|
6169
6526
|
if down_factor is not None:
|
|
6170
6527
|
|
|
6171
6528
|
self.parent().reset(nodes = True, id_overlay = True, edges = True)
|
|
@@ -6181,6 +6538,8 @@ class BranchDialog(QDialog):
|
|
|
6181
6538
|
|
|
6182
6539
|
except Exception as e:
|
|
6183
6540
|
print(f"Error labeling branches: {e}")
|
|
6541
|
+
import traceback
|
|
6542
|
+
print(traceback.format_exc())
|
|
6184
6543
|
|
|
6185
6544
|
|
|
6186
6545
|
|
|
@@ -6336,7 +6695,7 @@ class AlterDialog(QDialog):
|
|
|
6336
6695
|
class ModifyDialog(QDialog):
|
|
6337
6696
|
def __init__(self, parent=None):
|
|
6338
6697
|
super().__init__(parent)
|
|
6339
|
-
self.setWindowTitle("
|
|
6698
|
+
self.setWindowTitle("Modify Network Qualities")
|
|
6340
6699
|
self.setModal(True)
|
|
6341
6700
|
layout = QFormLayout(self)
|
|
6342
6701
|
|
|
@@ -6376,6 +6735,12 @@ class ModifyDialog(QDialog):
|
|
|
6376
6735
|
self.isolate.setChecked(False)
|
|
6377
6736
|
layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
|
|
6378
6737
|
|
|
6738
|
+
# Community collapse checkbox (default False)
|
|
6739
|
+
self.comcollapse = QPushButton("Communities -> nodes")
|
|
6740
|
+
self.comcollapse.setCheckable(True)
|
|
6741
|
+
self.comcollapse.setChecked(False)
|
|
6742
|
+
layout.addRow("Convert communities to nodes?:", self.comcollapse)
|
|
6743
|
+
|
|
6379
6744
|
#change button
|
|
6380
6745
|
change_button = QPushButton("Add/Remove Network Pairs")
|
|
6381
6746
|
change_button.clicked.connect(self.show_alter_dialog)
|
|
@@ -6409,6 +6774,8 @@ class ModifyDialog(QDialog):
|
|
|
6409
6774
|
edgeweight = self.edgeweight.isChecked()
|
|
6410
6775
|
prune = self.prune.isChecked()
|
|
6411
6776
|
isolate = self.isolate.isChecked()
|
|
6777
|
+
comcollapse = self.comcollapse.isChecked()
|
|
6778
|
+
|
|
6412
6779
|
|
|
6413
6780
|
if isolate and my_network.node_identities is not None:
|
|
6414
6781
|
self.show_isolate_dialog()
|
|
@@ -6434,6 +6801,15 @@ class ModifyDialog(QDialog):
|
|
|
6434
6801
|
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
6435
6802
|
except:
|
|
6436
6803
|
pass
|
|
6804
|
+
if comcollapse:
|
|
6805
|
+
if my_network.communities is None:
|
|
6806
|
+
self.parent().show_partition_dialog()
|
|
6807
|
+
if my_network.communities is None:
|
|
6808
|
+
return
|
|
6809
|
+
my_network.com_to_node()
|
|
6810
|
+
self.parent().load_channel(0, my_network.nodes, True)
|
|
6811
|
+
my_network.communities = None
|
|
6812
|
+
|
|
6437
6813
|
try:
|
|
6438
6814
|
if hasattr(my_network, 'network_lists'):
|
|
6439
6815
|
model = PandasModel(my_network.network_lists)
|
|
@@ -6552,8 +6928,7 @@ class CentroidDialog(QDialog):
|
|
|
6552
6928
|
"Error",
|
|
6553
6929
|
f"Error finding centroids: {str(e)}"
|
|
6554
6930
|
)
|
|
6555
|
-
|
|
6556
|
-
print(traceback.format_exc())
|
|
6931
|
+
|
|
6557
6932
|
|
|
6558
6933
|
|
|
6559
6934
|
|
nettracer3d/proximity.py
CHANGED
|
@@ -89,9 +89,11 @@ def process_label(args):
|
|
|
89
89
|
nodes, label, dilate_xy, dilate_z, array_shape = args
|
|
90
90
|
print(f"Processing node {label}")
|
|
91
91
|
indices = np.argwhere(nodes == label)
|
|
92
|
+
if len(indices) == 0:
|
|
93
|
+
return None, None
|
|
92
94
|
z_vals, y_vals, x_vals = get_reslice_indices((indices, dilate_xy, dilate_z, array_shape))
|
|
93
95
|
if z_vals is None: #If get_reslice_indices ran into a ValueError, nothing is returned.
|
|
94
|
-
return None, None
|
|
96
|
+
return None, None
|
|
95
97
|
sub_nodes = reslice_3d_array((nodes, z_vals, y_vals, x_vals))
|
|
96
98
|
return label, sub_nodes
|
|
97
99
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <boom2449@gmail.com>
|
|
6
6
|
Project-URL: User_Manual, https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link
|
|
@@ -23,6 +23,7 @@ Requires-Dist: pandas
|
|
|
23
23
|
Requires-Dist: napari
|
|
24
24
|
Requires-Dist: python-louvain
|
|
25
25
|
Requires-Dist: tifffile
|
|
26
|
+
Requires-Dist: qtrangeslider
|
|
26
27
|
Requires-Dist: PyQt6
|
|
27
28
|
Provides-Extra: cuda11
|
|
28
29
|
Requires-Dist: cupy-cuda11x; extra == "cuda11"
|
|
@@ -31,15 +32,8 @@ Requires-Dist: cupy-cuda12x; extra == "cuda12"
|
|
|
31
32
|
Provides-Extra: cupy
|
|
32
33
|
Requires-Dist: cupy; extra == "cupy"
|
|
33
34
|
|
|
34
|
-
NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity or volumetric thresholding. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip,
|
|
35
|
+
NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity or volumetric thresholding. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, enter the command 'nettracer3d' in your command prompt:
|
|
35
36
|
|
|
36
|
-
#Start
|
|
37
|
-
|
|
38
|
-
from nettracer3d import nettracer_gui
|
|
39
|
-
|
|
40
|
-
nettracer_gui.run_gui()
|
|
41
|
-
|
|
42
|
-
#End
|
|
43
37
|
|
|
44
38
|
This gui is built from the PyQt6 package and therefore may not function on dockers or virtual envs that are unable to support PyQt6 displays. More advanced documentation (especially for the GUI) is coming down the line, but for now please see: https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link
|
|
45
39
|
for a user manual that provides older documentation.
|
|
@@ -2,19 +2,19 @@ nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
nettracer3d/community_extractor.py,sha256=8bRDJOfZhOFLtpkJVaDQrQ4O8wUywyr-EfVvW5fxyPs,31700
|
|
3
3
|
nettracer3d/hub_getter.py,sha256=KiNtxdajLkwB1ftslvrh1FE1Ch9ZCFEmHSEEotwR-To,8298
|
|
4
4
|
nettracer3d/modularity.py,sha256=V1f3s_vGd8EuVz27mzq6ycIGr0BWIpH7c7NU4QjgAHU,30247
|
|
5
|
-
nettracer3d/morphology.py,sha256=
|
|
6
|
-
nettracer3d/nettracer.py,sha256=
|
|
7
|
-
nettracer3d/nettracer_gui.py,sha256=
|
|
5
|
+
nettracer3d/morphology.py,sha256=CsRWB0DY-vBBlKdF9IQwgfYYZswuE7n1Iu_Osxgmxnw,13042
|
|
6
|
+
nettracer3d/nettracer.py,sha256=eMQtt6vjhxLYhzIh91pKt3L-K6kKM55FWS-rOu72Vwc,207727
|
|
7
|
+
nettracer3d/nettracer_gui.py,sha256=ceVgsuIbWTQO8KqiNHrGJDPWQtMAH2NM-4sMdvYeKGY,305658
|
|
8
8
|
nettracer3d/network_analysis.py,sha256=MJBBjslA1k_R8ymid77U-qGSgzxFVfzGVQhE0IdhnbE,48046
|
|
9
9
|
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
10
10
|
nettracer3d/node_draw.py,sha256=BMiD_FrlOHeGD4AQZ_Emd152PfxFuMgGf2x4S0TOTnw,9752
|
|
11
|
-
nettracer3d/proximity.py,sha256=
|
|
11
|
+
nettracer3d/proximity.py,sha256=B1pmFegx5Wb0JKI5rvpILv2VRU09f6M2iljAQAqBja0,11059
|
|
12
12
|
nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
|
|
13
13
|
nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
|
|
14
14
|
nettracer3d/smart_dilate.py,sha256=howfO6Lw5PxNjkaOBSCjkmf7fyau_-_8iTct2mAuTAQ,22083
|
|
15
|
-
nettracer3d-0.4.
|
|
16
|
-
nettracer3d-0.4.
|
|
17
|
-
nettracer3d-0.4.
|
|
18
|
-
nettracer3d-0.4.
|
|
19
|
-
nettracer3d-0.4.
|
|
20
|
-
nettracer3d-0.4.
|
|
15
|
+
nettracer3d-0.4.3.dist-info/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
|
|
16
|
+
nettracer3d-0.4.3.dist-info/METADATA,sha256=mVVm2Jw0nwFcMa-fyk8ZuHS60qhFVDG2kqm9jUfbiIM,2835
|
|
17
|
+
nettracer3d-0.4.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
18
|
+
nettracer3d-0.4.3.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
19
|
+
nettracer3d-0.4.3.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
20
|
+
nettracer3d-0.4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|