nettracer3d 0.5.2__py3-none-any.whl → 0.5.4__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/community_extractor.py +42 -0
- nettracer3d/morphology.py +63 -109
- nettracer3d/nettracer.py +70 -49
- nettracer3d/nettracer_gui.py +378 -81
- nettracer3d/proximity.py +45 -47
- nettracer3d/segmenter.py +1 -1
- {nettracer3d-0.5.2.dist-info → nettracer3d-0.5.4.dist-info}/METADATA +10 -1
- nettracer3d-0.5.4.dist-info/RECORD +21 -0
- nettracer3d-0.5.2.dist-info/RECORD +0 -21
- {nettracer3d-0.5.2.dist-info → nettracer3d-0.5.4.dist-info}/LICENSE +0 -0
- {nettracer3d-0.5.2.dist-info → nettracer3d-0.5.4.dist-info}/WHEEL +0 -0
- {nettracer3d-0.5.2.dist-info → nettracer3d-0.5.4.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.5.2.dist-info → nettracer3d-0.5.4.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -397,6 +397,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
397
397
|
# Initialize highlight overlay
|
|
398
398
|
self.highlight_overlay = None
|
|
399
399
|
self.highlight_bounds = None # Store bounds for positioning
|
|
400
|
+
self.mini_overlay = False # If the program is currently drawing the overlay by frame this will be true
|
|
401
|
+
self.mini_thresh = (500*500*500) # Array volume to start using mini overlays for
|
|
400
402
|
|
|
401
403
|
def start_left_scroll(self):
|
|
402
404
|
"""Start scrolling left when left arrow is pressed."""
|
|
@@ -443,6 +445,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
443
445
|
edge_indices (list): List of edge indices to highlight
|
|
444
446
|
"""
|
|
445
447
|
|
|
448
|
+
self.mini_overlay = False #If this method is ever being called, it means we are rendering the entire overlay so mini overlay needs to reset.
|
|
449
|
+
|
|
446
450
|
def process_chunk(chunk_data, indices_to_check):
|
|
447
451
|
"""Process a single chunk of the array to create highlight mask"""
|
|
448
452
|
mask = np.isin(chunk_data, indices_to_check)
|
|
@@ -463,6 +467,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
463
467
|
if overlay1_indices is not None:
|
|
464
468
|
if 0 in overlay1_indices:
|
|
465
469
|
overlay1_indices.remove(0)
|
|
470
|
+
if overlay2_indices is not None:
|
|
471
|
+
if 0 in overlay2_indices:
|
|
472
|
+
overlay2_indices.remove(0)
|
|
466
473
|
|
|
467
474
|
if node_indices is None:
|
|
468
475
|
node_indices = []
|
|
@@ -628,6 +635,110 @@ class ImageViewerWindow(QMainWindow):
|
|
|
628
635
|
# Update display
|
|
629
636
|
self.update_display(preserve_zoom=(current_xlim, current_ylim), called = True)
|
|
630
637
|
|
|
638
|
+
def create_mini_overlay(self, node_indices = None, edge_indices = None):
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
Create a binary overlay highlighting specific nodes and/or edges using parallel processing, one slice at a time for efficiency.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
node_indices (list): List of node indices to highlight
|
|
645
|
+
edge_indices (list): List of edge indices to highlight
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
def process_chunk(chunk_data, indices_to_check):
|
|
649
|
+
"""Process a single chunk of the array to create highlight mask"""
|
|
650
|
+
mask = np.isin(chunk_data, indices_to_check)
|
|
651
|
+
return mask * 255
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
if node_indices is not None:
|
|
655
|
+
if 0 in node_indices:
|
|
656
|
+
node_indices.remove(0)
|
|
657
|
+
if edge_indices is not None:
|
|
658
|
+
if 0 in edge_indices:
|
|
659
|
+
edge_indices.remove(0)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
if node_indices is None:
|
|
663
|
+
node_indices = []
|
|
664
|
+
if edge_indices is None:
|
|
665
|
+
edge_indices = []
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
669
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
670
|
+
|
|
671
|
+
if not node_indices and not edge_indices:
|
|
672
|
+
self.highlight_overlay = None
|
|
673
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
# Get the shape of the full array from any existing channel
|
|
677
|
+
for channel in self.channel_data:
|
|
678
|
+
if channel is not None:
|
|
679
|
+
full_shape = channel.shape
|
|
680
|
+
break
|
|
681
|
+
else:
|
|
682
|
+
return # No valid channels to get shape from
|
|
683
|
+
|
|
684
|
+
# Initialize full-size overlay
|
|
685
|
+
if self.highlight_overlay is None:
|
|
686
|
+
self.highlight_overlay = np.zeros(full_shape, dtype=np.uint8)
|
|
687
|
+
|
|
688
|
+
# Get number of CPU cores
|
|
689
|
+
num_cores = mp.cpu_count()
|
|
690
|
+
|
|
691
|
+
# Calculate chunk size along y-axis
|
|
692
|
+
chunk_size = full_shape[1] // num_cores
|
|
693
|
+
if chunk_size < 1:
|
|
694
|
+
chunk_size = 1
|
|
695
|
+
|
|
696
|
+
def process_channel(channel_data, indices, array_shape):
|
|
697
|
+
if channel_data is None or not indices:
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
# Create chunks
|
|
701
|
+
chunks = []
|
|
702
|
+
for i in range(0, array_shape[1], chunk_size):
|
|
703
|
+
end = min(i + chunk_size, array_shape[1])
|
|
704
|
+
chunks.append(channel_data[i:end, :])
|
|
705
|
+
|
|
706
|
+
# Process chunks in parallel using ThreadPoolExecutor
|
|
707
|
+
process_func = partial(process_chunk, indices_to_check=indices)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
711
|
+
chunk_results = list(executor.map(process_func, chunks))
|
|
712
|
+
|
|
713
|
+
# Reassemble the chunks
|
|
714
|
+
return np.concatenate(chunk_results, axis=0)
|
|
715
|
+
|
|
716
|
+
# Process nodes and edges in parallel using multiprocessing
|
|
717
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
718
|
+
try:
|
|
719
|
+
slice_node = self.channel_data[0][self.current_slice, :, :] #This is the only major difference to the big highlight... we are only looking at this
|
|
720
|
+
future_nodes = executor.submit(process_channel, slice_node, node_indices, full_shape)
|
|
721
|
+
node_overlay = future_nodes.result()
|
|
722
|
+
except:
|
|
723
|
+
node_overlay = None
|
|
724
|
+
try:
|
|
725
|
+
slice_edge = self.channel_data[1][self.current_slice, :, :]
|
|
726
|
+
future_edges = executor.submit(process_channel, slice_edge, edge_indices, full_shape)
|
|
727
|
+
edge_overlay = future_edges.result()
|
|
728
|
+
except:
|
|
729
|
+
edge_overlay = None
|
|
730
|
+
|
|
731
|
+
# Combine results
|
|
732
|
+
self.highlight_overlay[self.current_slice, :, :] = np.zeros_like(self.highlight_overlay[self.current_slice, :, :])
|
|
733
|
+
if node_overlay is not None:
|
|
734
|
+
self.highlight_overlay[self.current_slice, :, :] = np.maximum(self.highlight_overlay[self.current_slice, :, :], node_overlay)
|
|
735
|
+
if edge_overlay is not None:
|
|
736
|
+
self.highlight_overlay[self.current_slice, :, :] = np.maximum(self.highlight_overlay[self.current_slice, :, :], edge_overlay)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
# Update display
|
|
740
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
741
|
+
|
|
631
742
|
|
|
632
743
|
|
|
633
744
|
|
|
@@ -890,14 +1001,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
890
1001
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
891
1002
|
self.clicked_values['edges'] = edge_indices
|
|
892
1003
|
|
|
893
|
-
self.
|
|
894
|
-
|
|
895
|
-
edge_indices=self.clicked_values['edges']
|
|
896
|
-
|
|
1004
|
+
if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
|
|
1005
|
+
self.mini_overlay = True
|
|
1006
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1007
|
+
else:
|
|
1008
|
+
self.create_highlight_overlay(
|
|
1009
|
+
node_indices=self.clicked_values['nodes'],
|
|
1010
|
+
edge_indices=self.clicked_values['edges']
|
|
1011
|
+
)
|
|
897
1012
|
else:
|
|
898
|
-
self.
|
|
899
|
-
|
|
900
|
-
|
|
1013
|
+
if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
|
|
1014
|
+
self.mini_overlay = True
|
|
1015
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1016
|
+
else:
|
|
1017
|
+
self.create_highlight_overlay(
|
|
1018
|
+
node_indices=self.clicked_values['nodes'],
|
|
1019
|
+
edge_indices = self.clicked_values['edges']
|
|
1020
|
+
)
|
|
901
1021
|
|
|
902
1022
|
|
|
903
1023
|
except Exception as e:
|
|
@@ -972,14 +1092,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
972
1092
|
if edges:
|
|
973
1093
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
974
1094
|
self.clicked_values['edges'] = edge_indices
|
|
975
|
-
self.
|
|
976
|
-
|
|
977
|
-
edge_indices=
|
|
978
|
-
|
|
1095
|
+
if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
|
|
1096
|
+
self.mini_overlay = True
|
|
1097
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1098
|
+
else:
|
|
1099
|
+
self.create_highlight_overlay(
|
|
1100
|
+
node_indices=self.clicked_values['nodes'],
|
|
1101
|
+
edge_indices=edge_indices
|
|
1102
|
+
)
|
|
979
1103
|
else:
|
|
980
|
-
self.
|
|
981
|
-
|
|
982
|
-
|
|
1104
|
+
if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
|
|
1105
|
+
self.mini_overlay = True
|
|
1106
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1107
|
+
else:
|
|
1108
|
+
self.create_highlight_overlay(
|
|
1109
|
+
node_indices = self.clicked_values['nodes'],
|
|
1110
|
+
edge_indices = self.clicked_values['edges']
|
|
1111
|
+
)
|
|
983
1112
|
|
|
984
1113
|
except Exception as e:
|
|
985
1114
|
|
|
@@ -1043,15 +1172,24 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1043
1172
|
if edges:
|
|
1044
1173
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
1045
1174
|
self.clicked_values['edges'] = edge_indices
|
|
1046
|
-
self.
|
|
1047
|
-
|
|
1048
|
-
edge_indices=edge_indices
|
|
1049
|
-
|
|
1175
|
+
if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
|
|
1176
|
+
self.mini_overlay = True
|
|
1177
|
+
self.create_mini_overlay(node_indices = nodes, edge_indices = edge_indices)
|
|
1178
|
+
else:
|
|
1179
|
+
self.create_highlight_overlay(
|
|
1180
|
+
node_indices=nodes,
|
|
1181
|
+
edge_indices=edge_indices
|
|
1182
|
+
)
|
|
1050
1183
|
self.clicked_values['nodes'] = nodes
|
|
1051
1184
|
else:
|
|
1052
|
-
self.
|
|
1053
|
-
|
|
1054
|
-
|
|
1185
|
+
if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
|
|
1186
|
+
self.mini_overlay = True
|
|
1187
|
+
self.create_mini_overlay(node_indices = nodes, edge_indices = self.clicked_values['edges'])
|
|
1188
|
+
else:
|
|
1189
|
+
self.create_highlight_overlay(
|
|
1190
|
+
node_indices = nodes,
|
|
1191
|
+
edge_indices = self.clicked_values['edges']
|
|
1192
|
+
)
|
|
1055
1193
|
self.clicked_values['nodes'] = nodes
|
|
1056
1194
|
|
|
1057
1195
|
except Exception as e:
|
|
@@ -1095,9 +1233,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1095
1233
|
|
|
1096
1234
|
print(f"Found {len(filtered_df)} direct connections between nodes of ID {sort} and their neighbors (of any ID)")
|
|
1097
1235
|
|
|
1098
|
-
self.
|
|
1099
|
-
|
|
1100
|
-
)
|
|
1236
|
+
if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
|
|
1237
|
+
self.mini_overlay = True
|
|
1238
|
+
self.create_mini_overlay(node_indices = nodes, edge_indices = self.clicked_values['edges'])
|
|
1239
|
+
else:
|
|
1240
|
+
self.create_highlight_overlay(
|
|
1241
|
+
node_indices = nodes,
|
|
1242
|
+
edge_indices = self.clicked_values['edges']
|
|
1243
|
+
)
|
|
1244
|
+
self.clicked_values['nodes'] = nodes
|
|
1101
1245
|
|
|
1102
1246
|
except Exception as e:
|
|
1103
1247
|
print(f"Error showing identities: {e}")
|
|
@@ -1286,7 +1430,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1286
1430
|
self.load_channel(1, my_network.edges, True)
|
|
1287
1431
|
self.highlight_overlay = None
|
|
1288
1432
|
self.update_display()
|
|
1289
|
-
print("Network is not updated automatically, please recompute if
|
|
1433
|
+
print("Network is not updated automatically, please recompute if necessary. Identities are not automatically updated.")
|
|
1290
1434
|
self.show_centroid_dialog()
|
|
1291
1435
|
|
|
1292
1436
|
except Exception as e:
|
|
@@ -1315,7 +1459,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1315
1459
|
|
|
1316
1460
|
|
|
1317
1461
|
if len(self.clicked_values['edges']) > 0:
|
|
1318
|
-
self.create_highlight_overlay(
|
|
1462
|
+
self.create_highlight_overlay(edge_indices = self.clicked_values['edges'])
|
|
1319
1463
|
mask = self.highlight_overlay == 0
|
|
1320
1464
|
my_network.edges = my_network.edges * mask
|
|
1321
1465
|
self.load_channel(1, my_network.edges, True)
|
|
@@ -1393,8 +1537,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1393
1537
|
|
|
1394
1538
|
pairs = list(combinations(nodes, 2))
|
|
1395
1539
|
|
|
1396
|
-
print(pairs)
|
|
1397
|
-
|
|
1398
1540
|
|
|
1399
1541
|
for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
|
|
1400
1542
|
print((my_network.network_lists[0][i], my_network.network_lists[1][i]))
|
|
@@ -1891,7 +2033,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1891
2033
|
self.clicked_values['nodes'].extend(selected_values)
|
|
1892
2034
|
# Remove duplicates while preserving order
|
|
1893
2035
|
self.clicked_values['nodes'] = list(dict.fromkeys(self.clicked_values['nodes']))
|
|
1894
|
-
self.
|
|
2036
|
+
if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
|
|
2037
|
+
self.mini_overlay = True
|
|
2038
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2039
|
+
else:
|
|
2040
|
+
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
|
|
1895
2041
|
|
|
1896
2042
|
# Try to highlight the last selected value in tables
|
|
1897
2043
|
if self.clicked_values['nodes']:
|
|
@@ -1903,7 +2049,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1903
2049
|
self.clicked_values['edges'].extend(selected_values)
|
|
1904
2050
|
# Remove duplicates while preserving order
|
|
1905
2051
|
self.clicked_values['edges'] = list(dict.fromkeys(self.clicked_values['edges']))
|
|
1906
|
-
self.
|
|
2052
|
+
if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
|
|
2053
|
+
self.mini_overlay = True
|
|
2054
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2055
|
+
else:
|
|
2056
|
+
self.create_highlight_overlay(edge_indices=self.clicked_values['edges'])
|
|
1907
2057
|
|
|
1908
2058
|
# Try to highlight the last selected value in tables
|
|
1909
2059
|
if self.clicked_values['edges']:
|
|
@@ -2144,9 +2294,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2144
2294
|
|
|
2145
2295
|
# Highlight the clicked element in the image using the stored lists
|
|
2146
2296
|
if self.active_channel == 0 and (starting_vals['nodes']) != (self.clicked_values['nodes']):
|
|
2147
|
-
self.
|
|
2297
|
+
if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
|
|
2298
|
+
self.mini_overlay = True
|
|
2299
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2300
|
+
else:
|
|
2301
|
+
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'], edge_indices=self.clicked_values['edges'])
|
|
2148
2302
|
elif self.active_channel == 1 and starting_vals['edges'] != self.clicked_values['edges']:
|
|
2149
|
-
self.
|
|
2303
|
+
if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
|
|
2304
|
+
self.mini_overlay = True
|
|
2305
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2306
|
+
else:
|
|
2307
|
+
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'], edge_indices=self.clicked_values['edges'])
|
|
2150
2308
|
|
|
2151
2309
|
|
|
2152
2310
|
except IndexError:
|
|
@@ -2298,6 +2456,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2298
2456
|
netoverlay_action.triggered.connect(self.show_netoverlay_dialog)
|
|
2299
2457
|
idoverlay_action = overlay_menu.addAction("Create ID Overlay")
|
|
2300
2458
|
idoverlay_action.triggered.connect(self.show_idoverlay_dialog)
|
|
2459
|
+
coloroverlay_action = overlay_menu.addAction("Color Nodes (or Edges)")
|
|
2460
|
+
coloroverlay_action.triggered.connect(self.show_coloroverlay_dialog)
|
|
2301
2461
|
searchoverlay_action = overlay_menu.addAction("Show Search Regions")
|
|
2302
2462
|
searchoverlay_action.triggered.connect(self.show_search_dialog)
|
|
2303
2463
|
shuffle_action = overlay_menu.addAction("Shuffle")
|
|
@@ -2578,6 +2738,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2578
2738
|
dialog = IdOverlayDialog(self)
|
|
2579
2739
|
dialog.exec()
|
|
2580
2740
|
|
|
2741
|
+
def show_coloroverlay_dialog(self):
|
|
2742
|
+
"""show the color overlay dialog"""
|
|
2743
|
+
dialog = ColorOverlayDialog(self)
|
|
2744
|
+
dialog.exec()
|
|
2745
|
+
|
|
2581
2746
|
def show_search_dialog(self):
|
|
2582
2747
|
"""Show the search dialog"""
|
|
2583
2748
|
dialog = SearchOverlayDialog(self)
|
|
@@ -2954,14 +3119,46 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2954
3119
|
|
|
2955
3120
|
try:
|
|
2956
3121
|
if not data: # For solo loading
|
|
2957
|
-
import tifffile
|
|
2958
3122
|
filename, _ = QFileDialog.getOpenFileName(
|
|
2959
3123
|
self,
|
|
2960
3124
|
f"Load Channel {channel_index + 1}",
|
|
2961
3125
|
"",
|
|
2962
|
-
"
|
|
3126
|
+
"Image Files (*.tif *.tiff *.nii *.jpg *.jpeg *.png)"
|
|
2963
3127
|
)
|
|
2964
|
-
|
|
3128
|
+
|
|
3129
|
+
if not filename:
|
|
3130
|
+
return
|
|
3131
|
+
|
|
3132
|
+
file_extension = filename.lower().split('.')[-1]
|
|
3133
|
+
|
|
3134
|
+
try:
|
|
3135
|
+
if file_extension in ['tif', 'tiff']:
|
|
3136
|
+
import tifffile
|
|
3137
|
+
self.channel_data[channel_index] = tifffile.imread(filename)
|
|
3138
|
+
|
|
3139
|
+
elif file_extension == 'nii':
|
|
3140
|
+
import nibabel as nib
|
|
3141
|
+
nii_img = nib.load(filename)
|
|
3142
|
+
# Get data and transpose to match TIFF orientation
|
|
3143
|
+
# If X needs to become Z, we move axis 2 (X) to position 0 (Z)
|
|
3144
|
+
data = nii_img.get_fdata()
|
|
3145
|
+
self.channel_data[channel_index] = np.transpose(data, (2, 1, 0))
|
|
3146
|
+
|
|
3147
|
+
elif file_extension in ['jpg', 'jpeg', 'png']:
|
|
3148
|
+
from PIL import Image
|
|
3149
|
+
|
|
3150
|
+
with Image.open(filename) as img:
|
|
3151
|
+
# Convert directly to numpy array, keeping color if present
|
|
3152
|
+
self.channel_data[channel_index] = np.array(img)
|
|
3153
|
+
|
|
3154
|
+
# Debug info to check shape
|
|
3155
|
+
print(f"Loaded image shape: {self.channel_data[channel_index].shape}")
|
|
3156
|
+
|
|
3157
|
+
except ImportError as e:
|
|
3158
|
+
QMessageBox.critical(self, "Error", f"Required library not installed: {str(e)}")
|
|
3159
|
+
except Exception as e:
|
|
3160
|
+
QMessageBox.critical(self, "Error", f"Error loading image: {str(e)}")
|
|
3161
|
+
|
|
2965
3162
|
|
|
2966
3163
|
if len(self.channel_data[channel_index].shape) == 2: # handle 2d data
|
|
2967
3164
|
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
@@ -2969,24 +3166,33 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2969
3166
|
else:
|
|
2970
3167
|
self.channel_data[channel_index] = channel_data
|
|
2971
3168
|
|
|
2972
|
-
|
|
2973
|
-
if self.channel_data[channel_index].shape
|
|
2974
|
-
if self.
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
3169
|
+
try:
|
|
3170
|
+
if len(self.channel_data[channel_index].shape) == 3: # potentially 2D RGB
|
|
3171
|
+
if self.channel_data[channel_index].shape[-1] in (3, 4): # last dim is 3 or 4
|
|
3172
|
+
if self.confirm_rgb_dialog():
|
|
3173
|
+
# User confirmed it's 2D RGB, expand to 4D
|
|
3174
|
+
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
3175
|
+
except:
|
|
3176
|
+
pass
|
|
3177
|
+
|
|
3178
|
+
try:
|
|
3179
|
+
if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
|
|
3180
|
+
self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index])
|
|
3181
|
+
except:
|
|
3182
|
+
pass
|
|
2980
3183
|
|
|
2981
3184
|
reset_resize = False
|
|
2982
3185
|
|
|
2983
3186
|
for i in range(4): #Try to ensure users don't load in different sized arrays
|
|
2984
3187
|
if self.channel_data[i] is None or i == channel_index or data:
|
|
2985
3188
|
if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
|
|
2986
|
-
|
|
2987
|
-
self.
|
|
2988
|
-
|
|
2989
|
-
|
|
3189
|
+
try:
|
|
3190
|
+
if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
|
|
3191
|
+
self.resizing = True
|
|
3192
|
+
reset_resize = True
|
|
3193
|
+
self.highlight_overlay = None
|
|
3194
|
+
except:
|
|
3195
|
+
pass
|
|
2990
3196
|
continue
|
|
2991
3197
|
else:
|
|
2992
3198
|
old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
|
|
@@ -3015,27 +3221,30 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3015
3221
|
self.active_channel_combo.setEnabled(True)
|
|
3016
3222
|
|
|
3017
3223
|
# Update slider range if this is the first channel loaded
|
|
3018
|
-
|
|
3019
|
-
if
|
|
3020
|
-
self.slice_slider.
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3224
|
+
try:
|
|
3225
|
+
if len(self.channel_data[channel_index].shape) == 3 or len(self.channel_data[channel_index].shape) == 4:
|
|
3226
|
+
if not self.slice_slider.isEnabled():
|
|
3227
|
+
self.slice_slider.setEnabled(True)
|
|
3228
|
+
self.slice_slider.setMinimum(0)
|
|
3229
|
+
self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
|
|
3230
|
+
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
3231
|
+
self.current_slice = self.slice_slider.value()
|
|
3232
|
+
else:
|
|
3233
|
+
self.slice_slider.setValue(0)
|
|
3234
|
+
self.current_slice = 0
|
|
3025
3235
|
else:
|
|
3026
|
-
self.slice_slider.
|
|
3027
|
-
self.
|
|
3236
|
+
self.slice_slider.setEnabled(True)
|
|
3237
|
+
self.slice_slider.setMinimum(0)
|
|
3238
|
+
self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
|
|
3239
|
+
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
3240
|
+
self.current_slice = self.slice_slider.value()
|
|
3241
|
+
else:
|
|
3242
|
+
self.current_slice = 0
|
|
3243
|
+
self.slice_slider.setValue(0)
|
|
3028
3244
|
else:
|
|
3029
|
-
self.slice_slider.setEnabled(
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
3033
|
-
self.current_slice = self.slice_slider.value()
|
|
3034
|
-
else:
|
|
3035
|
-
self.current_slice = 0
|
|
3036
|
-
self.slice_slider.setValue(0)
|
|
3037
|
-
else:
|
|
3038
|
-
self.slice_slider.setEnabled(False)
|
|
3245
|
+
self.slice_slider.setEnabled(False)
|
|
3246
|
+
except:
|
|
3247
|
+
pass
|
|
3039
3248
|
|
|
3040
3249
|
|
|
3041
3250
|
# If this is the first channel loaded, make it active
|
|
@@ -3048,19 +3257,24 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3048
3257
|
self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
|
|
3049
3258
|
self.volume_dict[channel_index] = None #reset volumes
|
|
3050
3259
|
|
|
3051
|
-
|
|
3052
|
-
if
|
|
3053
|
-
self.original_shape
|
|
3054
|
-
|
|
3055
|
-
self.original_shape
|
|
3056
|
-
|
|
3057
|
-
|
|
3260
|
+
try:
|
|
3261
|
+
if assign_shape: #keep original shape tracked to undo resampling.
|
|
3262
|
+
if self.original_shape is None:
|
|
3263
|
+
self.original_shape = self.channel_data[channel_index].shape
|
|
3264
|
+
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]:
|
|
3265
|
+
self.original_shape = self.channel_data[channel_index].shape
|
|
3266
|
+
if len(self.original_shape) == 4:
|
|
3267
|
+
self.original_shape = (self.original_shape[0], self.original_shape[1], self.original_shape[2])
|
|
3268
|
+
except:
|
|
3269
|
+
pass
|
|
3058
3270
|
|
|
3059
3271
|
self.update_display(reset_resize = reset_resize)
|
|
3060
3272
|
|
|
3061
3273
|
|
|
3062
3274
|
|
|
3063
3275
|
except Exception as e:
|
|
3276
|
+
import traceback
|
|
3277
|
+
print(traceback.format_exc())
|
|
3064
3278
|
if not data:
|
|
3065
3279
|
from PyQt6.QtWidgets import QMessageBox
|
|
3066
3280
|
QMessageBox.critical(
|
|
@@ -3225,6 +3439,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3225
3439
|
elif ch_index == 3:
|
|
3226
3440
|
my_network.save_id_overlay(filename=filename)
|
|
3227
3441
|
elif ch_index == 4:
|
|
3442
|
+
if self.mini_overlay == True:
|
|
3443
|
+
self.create_highlight_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
3228
3444
|
if filename == None:
|
|
3229
3445
|
filename = "Highlighted_Element.tif"
|
|
3230
3446
|
tifffile.imwrite(f"{filename}", self.highlight_overlay)
|
|
@@ -3269,6 +3485,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3269
3485
|
if self.pending_slice is not None:
|
|
3270
3486
|
slice_value, view_settings = self.pending_slice
|
|
3271
3487
|
self.current_slice = slice_value
|
|
3488
|
+
if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
|
|
3489
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
3272
3490
|
self.update_display(preserve_zoom=view_settings)
|
|
3273
3491
|
self.pending_slice = None
|
|
3274
3492
|
|
|
@@ -3931,7 +4149,13 @@ class CustomTableView(QTableView):
|
|
|
3931
4149
|
# Navigate to the Z-slice
|
|
3932
4150
|
self.parent.slice_slider.setValue(int(centroid[0]))
|
|
3933
4151
|
print(f"Found node {value} at Z-slice {centroid[0]}")
|
|
3934
|
-
self.parent.
|
|
4152
|
+
if self.parent.channel_data[0].shape[0] * self.parent.channel_data[0].shape[1] * self.parent.channel_data[0].shape[2] > self.parent.mini_thresh:
|
|
4153
|
+
self.parent.mini_overlay = True
|
|
4154
|
+
self.parent.create_mini_overlay(node_indices = [value])
|
|
4155
|
+
else:
|
|
4156
|
+
self.parent.create_highlight_overlay(node_indices=[value])
|
|
4157
|
+
self.parent.clicked_values['nodes'] = []
|
|
4158
|
+
self.parent.clicked_values['edges'] = []
|
|
3935
4159
|
self.parent.clicked_values['nodes'].append(value)
|
|
3936
4160
|
|
|
3937
4161
|
# Highlight the value in both tables if it exists
|
|
@@ -3955,7 +4179,13 @@ class CustomTableView(QTableView):
|
|
|
3955
4179
|
# Navigate to the Z-slice
|
|
3956
4180
|
self.parent.slice_slider.setValue(int(centroid[0]))
|
|
3957
4181
|
print(f"Found edge {value} at Z-slice {centroid[0]}")
|
|
3958
|
-
self.parent.
|
|
4182
|
+
if self.parent.channel_data[1].shape[0] * self.parent.channel_data[1].shape[1] * self.parent.channel_data[1].shape[2] > self.parent.mini_thresh:
|
|
4183
|
+
self.parent.mini_overlay = True
|
|
4184
|
+
self.parent.create_mini_overlay(edge_indices = [value])
|
|
4185
|
+
else:
|
|
4186
|
+
self.parent.create_highlight_overlay(edge_indices=[value])
|
|
4187
|
+
self.parent.clicked_values['nodes'] = []
|
|
4188
|
+
self.parent.clicked_values['edges'] = []
|
|
3959
4189
|
self.parent.clicked_values['edges'].append(value)
|
|
3960
4190
|
|
|
3961
4191
|
# Highlight the value in both tables if it exists
|
|
@@ -3983,12 +4213,24 @@ class CustomTableView(QTableView):
|
|
|
3983
4213
|
self.parent.slice_slider.setValue(int(centroid1[0]))
|
|
3984
4214
|
print(f"Found node pair {value[0]} and {value[1]} at Z-slices {centroid1[0]} and {centroid2[0]}, respectively")
|
|
3985
4215
|
try:
|
|
3986
|
-
self.parent.
|
|
4216
|
+
if self.parent.channel_data[0].shape[0] * self.parent.channel_data[0].shape[1] * self.parent.channel_data[0].shape[2] > self.parent.mini_thresh:
|
|
4217
|
+
self.parent.mini_overlay = True
|
|
4218
|
+
self.parent.create_mini_overlay(node_indices=[int(value[0]), int(value[1])], edge_indices = int(value[2]))
|
|
4219
|
+
else:
|
|
4220
|
+
self.parent.create_highlight_overlay(node_indices=[int(value[0]), int(value[1])], edge_indices = int(value[2]))
|
|
4221
|
+
self.parent.clicked_values['nodes'] = []
|
|
4222
|
+
self.parent.clicked_values['edges'] = []
|
|
3987
4223
|
self.parent.clicked_values['edges'].append(value[2])
|
|
3988
4224
|
self.parent.clicked_values['nodes'].append(value[0])
|
|
3989
4225
|
self.parent.clicked_values['nodes'].append(value[1])
|
|
3990
4226
|
except:
|
|
3991
|
-
self.parent.
|
|
4227
|
+
if self.parent.channel_data[0].shape[0] * self.parent.channel_data[0].shape[1] * self.parent.channel_data[0].shape[2] > self.parent.mini_thresh:
|
|
4228
|
+
self.parent.mini_overlay = True
|
|
4229
|
+
self.parent.create_mini_overlay(node_indices=[int(value[0]), int(value[1])])
|
|
4230
|
+
else:
|
|
4231
|
+
self.parent.create_highlight_overlay(node_indices=[int(value[0]), int(value[1])])
|
|
4232
|
+
self.parent.clicked_values['nodes'] = []
|
|
4233
|
+
self.parent.clicked_values['edges'] = []
|
|
3992
4234
|
self.parent.clicked_values['nodes'].append(value[0])
|
|
3993
4235
|
self.parent.clicked_values['nodes'].append(value[1])
|
|
3994
4236
|
|
|
@@ -4534,6 +4776,8 @@ class Show3dDialog(QDialog):
|
|
|
4534
4776
|
arrays_4d.append(channel)
|
|
4535
4777
|
|
|
4536
4778
|
if self.parent().highlight_overlay is not None:
|
|
4779
|
+
if self.parent().mini_overlay == True:
|
|
4780
|
+
self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
4537
4781
|
arrays_3d.append(self.parent().highlight_overlay)
|
|
4538
4782
|
colors.append(color_template[4])
|
|
4539
4783
|
|
|
@@ -4668,6 +4912,45 @@ class IdOverlayDialog(QDialog):
|
|
|
4668
4912
|
|
|
4669
4913
|
self.accept()
|
|
4670
4914
|
|
|
4915
|
+
class ColorOverlayDialog(QDialog):
|
|
4916
|
+
|
|
4917
|
+
def __init__(self, parent=None):
|
|
4918
|
+
|
|
4919
|
+
super().__init__(parent)
|
|
4920
|
+
self.setWindowTitle("Generate Node (or Edge) -> Color Overlay?")
|
|
4921
|
+
self.setModal(True)
|
|
4922
|
+
|
|
4923
|
+
layout = QFormLayout(self)
|
|
4924
|
+
|
|
4925
|
+
self.down_factor = QLineEdit("")
|
|
4926
|
+
layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
|
|
4927
|
+
|
|
4928
|
+
# Add Run button
|
|
4929
|
+
run_button = QPushButton("Generate (Will go to Overlay 2)")
|
|
4930
|
+
run_button.clicked.connect(self.coloroverlay)
|
|
4931
|
+
layout.addWidget(run_button)
|
|
4932
|
+
|
|
4933
|
+
def coloroverlay(self):
|
|
4934
|
+
|
|
4935
|
+
down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
|
|
4936
|
+
|
|
4937
|
+
if self.parent().active_channel == 0:
|
|
4938
|
+
mode = 0
|
|
4939
|
+
self.sort = 'Node'
|
|
4940
|
+
else:
|
|
4941
|
+
mode = 1
|
|
4942
|
+
self.sort = 'Edge'
|
|
4943
|
+
|
|
4944
|
+
|
|
4945
|
+
result, legend = my_network.node_to_color(down_factor = down_factor, mode = mode)
|
|
4946
|
+
|
|
4947
|
+
self.parent().format_for_upperright_table(legend, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
|
|
4948
|
+
|
|
4949
|
+
|
|
4950
|
+
self.parent().load_channel(3, channel_data = result, data = True)
|
|
4951
|
+
|
|
4952
|
+
self.accept()
|
|
4953
|
+
|
|
4671
4954
|
|
|
4672
4955
|
class ShuffleDialog(QDialog):
|
|
4673
4956
|
|
|
@@ -4708,11 +4991,15 @@ class ShuffleDialog(QDialog):
|
|
|
4708
4991
|
accepted_target = self.target_selector.currentIndex()
|
|
4709
4992
|
|
|
4710
4993
|
if accepted_mode == 4:
|
|
4994
|
+
if self.parent().mini_overlay == True:
|
|
4995
|
+
self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
4711
4996
|
active_data = self.parent().highlight_overlay
|
|
4712
4997
|
else:
|
|
4713
4998
|
active_data = self.parent().channel_data[accepted_mode]
|
|
4714
4999
|
|
|
4715
5000
|
if accepted_target == 4:
|
|
5001
|
+
if self.parent().mini_overlay == True:
|
|
5002
|
+
self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
4716
5003
|
target_data = self.parent().highlight_overlay
|
|
4717
5004
|
else:
|
|
4718
5005
|
target_data = self.parent().channel_data[accepted_target]
|
|
@@ -6814,6 +7101,8 @@ class MaskDialog(QDialog):
|
|
|
6814
7101
|
output_target = self.output_selector.currentIndex()
|
|
6815
7102
|
|
|
6816
7103
|
if accepted_mode == 4:
|
|
7104
|
+
if self.parent().mini_overlay == True:
|
|
7105
|
+
self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
6817
7106
|
active_data = self.parent().highlight_overlay
|
|
6818
7107
|
else:
|
|
6819
7108
|
active_data = self.parent().channel_data[accepted_mode]
|
|
@@ -6916,11 +7205,16 @@ class WatershedDialog(QDialog):
|
|
|
6916
7205
|
self.directory.setPlaceholderText("Leave empty for None")
|
|
6917
7206
|
layout.addRow("Output Directory:", self.directory)
|
|
6918
7207
|
|
|
6919
|
-
|
|
7208
|
+
try:
|
|
6920
7209
|
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
7210
|
+
active_shape = self.parent().channel_data[self.parent().active_channel].shape[0]
|
|
7211
|
+
|
|
7212
|
+
if active_shape == 1:
|
|
7213
|
+
self.default = 0.2
|
|
7214
|
+
else:
|
|
7215
|
+
self.default = 0.05
|
|
7216
|
+
|
|
7217
|
+
except:
|
|
6924
7218
|
self.default = 0.05
|
|
6925
7219
|
|
|
6926
7220
|
|
|
@@ -6949,6 +7243,9 @@ class WatershedDialog(QDialog):
|
|
|
6949
7243
|
self.predownsample2.setPlaceholderText("Leave empty for None")
|
|
6950
7244
|
layout.addRow("Smart Label GPU Downsample:", self.predownsample2)
|
|
6951
7245
|
|
|
7246
|
+
layout.addRow("Note:", QLabel(f"If the optimal proportion watershed output is still labeling spatially seperated objects with the same label, try right placing the result in nodes or edges\nthen right click the image and choose 'select all', followed by right clicking and 'selection' -> 'split non-touching labels'."))
|
|
7247
|
+
|
|
7248
|
+
|
|
6952
7249
|
# Add Run button
|
|
6953
7250
|
run_button = QPushButton("Run Watershed")
|
|
6954
7251
|
run_button.clicked.connect(self.run_watershed)
|