nettracer3d 0.2.7__py3-none-any.whl → 0.2.8__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.
@@ -2,7 +2,7 @@ import pandas as pd
2
2
  import networkx as nx
3
3
  import tifffile
4
4
  import numpy as np
5
- from typing import List, Dict, Tuple
5
+ from typing import List, Dict, Tuple, Union, Any
6
6
  from collections import defaultdict, Counter
7
7
  from networkx.algorithms import community
8
8
  from scipy import ndimage
@@ -648,7 +648,69 @@ def find_hub_nodes(G: nx.Graph, proportion: float = 0.1) -> List:
648
648
 
649
649
  return hub_nodes
650
650
 
651
+ def get_color_name_mapping():
652
+ """Return a dictionary of common colors and their RGB values."""
653
+ return {
654
+ 'red': (255, 0, 0),
655
+ 'green': (0, 255, 0),
656
+ 'blue': (0, 0, 255),
657
+ 'yellow': (255, 255, 0),
658
+ 'cyan': (0, 255, 255),
659
+ 'magenta': (255, 0, 255),
660
+ 'purple': (128, 0, 128),
661
+ 'orange': (255, 165, 0),
662
+ 'brown': (165, 42, 42),
663
+ 'pink': (255, 192, 203),
664
+ 'navy': (0, 0, 128),
665
+ 'teal': (0, 128, 128),
666
+ 'olive': (128, 128, 0),
667
+ 'maroon': (128, 0, 0),
668
+ 'lime': (50, 205, 50),
669
+ 'indigo': (75, 0, 130),
670
+ 'violet': (238, 130, 238),
671
+ 'coral': (255, 127, 80),
672
+ 'turquoise': (64, 224, 208),
673
+ 'gold': (255, 215, 0)
674
+ }
675
+
676
+ def rgb_to_color_name(rgb: Tuple[int, int, int]) -> str:
677
+ """
678
+ Convert an RGB tuple to its nearest color name.
679
+
680
+ Args:
681
+ rgb: Tuple of (r, g, b) values
682
+
683
+ Returns:
684
+ str: Name of the closest matching color
685
+ """
686
+ color_map = get_color_name_mapping()
687
+
688
+ # Convert input RGB to numpy array
689
+ rgb_array = np.array(rgb)
690
+
691
+ # Calculate Euclidean distance to all known colors
692
+ min_distance = float('inf')
693
+ closest_color = None
694
+
695
+ for color_name, color_rgb in color_map.items():
696
+ distance = np.sqrt(np.sum((rgb_array - np.array(color_rgb)) ** 2))
697
+ if distance < min_distance:
698
+ min_distance = distance
699
+ closest_color = color_name
700
+
701
+ return closest_color
651
702
 
703
+ def convert_node_colors_to_names(node_to_color: Dict[int, Tuple[int, int, int]]) -> Dict[int, str]:
704
+ """
705
+ Convert a dictionary of node-to-RGB mappings to node-to-color-name mappings.
706
+
707
+ Args:
708
+ node_to_color: Dictionary mapping node IDs to RGB tuples
709
+
710
+ Returns:
711
+ Dictionary mapping node IDs to color names
712
+ """
713
+ return {node: rgb_to_color_name(color) for node, color in node_to_color.items()}
652
714
 
653
715
  def generate_distinct_colors(n_colors: int) -> List[Tuple[int, int, int]]:
654
716
  """
@@ -721,33 +783,55 @@ def assign_community_colors(community_dict: Dict[int, int], labeled_array: np.nd
721
783
  mask = labeled_array == label
722
784
  for i in range(3): # RGB channels
723
785
  rgb_array[mask, i] = node_to_color[label][i]
786
+
787
+ node_to_color_names = convert_node_colors_to_names(community_to_color)
788
+
724
789
 
725
- return rgb_array
790
+ return rgb_array, node_to_color_names
726
791
 
727
- def assign_community_grays(community_dict: Dict[int, int], labeled_array: np.ndarray) -> np.ndarray:
792
+ def assign_community_grays(community_dict: Dict[int, Union[int, str, Any]], labeled_array: np.ndarray) -> np.ndarray:
728
793
  """
729
- Assign distinct grayscale values to communities.
794
+ Assign grayscale values to communities. For numeric communities, uses the community
795
+ number directly. For string/other communities, assigns sequential values.
730
796
 
731
797
  Args:
732
- community_dict: Dictionary mapping node IDs to community numbers
798
+ community_dict: Dictionary mapping node IDs to community identifiers (numbers or strings)
733
799
  labeled_array: 3D numpy array with labels corresponding to node IDs
734
800
 
735
801
  Returns:
736
- grayscale numpy array
802
+ tuple: (grayscale numpy array, mapping of node IDs to assigned values)
737
803
  """
738
- # Get unique communities
739
- communities = set(community_dict.values())
740
- n_communities = len(communities)
804
+ # Determine if we're dealing with numeric or string communities
805
+ sample_value = next(iter(community_dict.values()))
806
+ is_numeric = isinstance(sample_value, (int, float))
741
807
 
742
- # Generate evenly spaced grayscale values (excluding pure black for background)
743
- gray_values = np.linspace(1, 255, n_communities, dtype=np.uint8)
808
+ if is_numeric:
809
+ # For numeric communities, use values directly
810
+ node_to_gray = community_dict
811
+ max_val = max(community_dict.values())
812
+ else:
813
+ # For string/other communities, assign sequential values
814
+ unique_communities = sorted(set(community_dict.values()))
815
+ community_to_value = {comm: i+1 for i, comm in enumerate(unique_communities)}
816
+ node_to_gray = {node: community_to_value[comm] for node, comm in community_dict.items()}
817
+ max_val = len(unique_communities)
744
818
 
745
- # Create direct mapping from node ID to grayscale value
746
- node_to_gray = {node: gray_values[list(communities).index(comm)]
747
- for node, comm in community_dict.items()}
819
+ # Choose appropriate dtype based on maximum value
820
+ if max_val <= 255:
821
+ dtype = np.uint8
822
+ elif max_val <= 65535:
823
+ dtype = np.uint16
824
+ else:
825
+ dtype = np.uint32
748
826
 
749
827
  # Create output array
750
- gray_array = np.zeros_like(labeled_array, dtype=np.uint8)
828
+ gray_array = np.zeros_like(labeled_array, dtype=dtype)
829
+
830
+ # Create mapping of unique communities to their grayscale values
831
+ if is_numeric:
832
+ community_to_gray = {comm: comm for comm in set(community_dict.values())}
833
+ else:
834
+ community_to_gray = {comm: i+1 for i, comm in enumerate(sorted(set(community_dict.values())))}
751
835
 
752
836
  # Use numpy's vectorized operations for faster assignment
753
837
  unique_labels = np.unique(labeled_array)
@@ -755,7 +839,7 @@ def assign_community_grays(community_dict: Dict[int, int], labeled_array: np.nda
755
839
  if label in node_to_gray:
756
840
  gray_array[labeled_array == label] = node_to_gray[label]
757
841
 
758
- return gray_array
842
+ return gray_array, community_to_gray
759
843
 
760
844
 
761
845
  if __name__ == "__main__":
nettracer3d/morphology.py CHANGED
@@ -270,51 +270,56 @@ def calculate_voxel_volumes(array, xy_scale=1, z_scale=1):
270
270
  return volumes
271
271
 
272
272
 
273
- def search_neighbor_ids(nodes, targets, id_dict, neighborhood_dict, totals, search, xy_scale, z_scale):
273
+ def search_neighbor_ids(nodes, targets, id_dict, neighborhood_dict, totals, search, xy_scale, z_scale, root):
274
274
 
275
-
275
+ if 0 in targets:
276
+ targets.remove(0)
276
277
  targets = np.isin(nodes, targets)
277
278
  targets = nettracer.binarize(targets)
278
279
 
279
280
  dilate_xy, dilate_z = nettracer.dilation_length_to_pixels(xy_scale, z_scale, search, search)
280
- print(f"Dilation parameters - xy: {dilate_xy}, z: {dilate_z}")
281
281
 
282
- targets = nettracer.dilate_3D_recursive(targets, dilate_xy, dilate_xy, dilate_z)
283
- targets = targets != 0
284
- print(f"After dilation - targets shape: {targets.shape}, sum: {np.sum(targets)}")
282
+ dilated = nettracer.dilate_3D_recursive(targets, dilate_xy, dilate_xy, dilate_z)
283
+ dilated = dilated - targets #technically we dont need the cores
284
+ search_vol = np.count_nonzero(dilated) * xy_scale * xy_scale * z_scale #need this for density
285
+ targets = dilated != 0
286
+ del dilated
287
+
285
288
 
286
289
  targets = targets * nodes
287
- print(f"After multiplication with nodes - unique values in targets: {np.unique(targets)}")
288
290
 
289
291
  unique, counts = np.unique(targets, return_counts=True)
290
292
  count_dict = dict(zip(unique, counts))
291
- print(f"Initial count_dict: {count_dict}")
293
+ print(count_dict)
292
294
 
293
295
  del count_dict[0]
294
- print(f"count_dict after removing zeros: {count_dict}")
295
296
 
296
297
  unique, counts = np.unique(nodes, return_counts=True)
297
298
  total_dict = dict(zip(unique, counts))
298
- print(f"Initial total_dict: {total_dict}")
299
-
299
+ print(total_dict)
300
+
300
301
  del total_dict[0]
301
- print(f"total_dict after removing zeros: {total_dict}")
302
302
 
303
- print(f"id_dict keys: {list(id_dict.keys())}")
304
- print(f"Initial neighborhood_dict: {neighborhood_dict}")
305
- print(f"Initial totals: {totals}")
306
303
 
307
304
  for label in total_dict:
308
305
  if label in id_dict:
309
306
  if label in count_dict:
310
307
  neighborhood_dict[id_dict[label]] += count_dict[label]
311
- print(f"Updated neighborhood_dict[{id_dict[label]}] with count {count_dict[label]}")
312
308
  totals[id_dict[label]] += total_dict[label]
313
- print(f"Updated totals[{id_dict[label]}] with total {total_dict[label]}")
309
+
310
+
311
+ try:
312
+ del neighborhood_dict[root] #no good way to get this
313
+ del totals[root] #no good way to get this
314
+ except:
315
+ pass
314
316
 
315
- print(f"Final neighborhood_dict: {neighborhood_dict}")
316
- print(f"Final totals: {totals}")
317
- return neighborhood_dict, totals
317
+ volume = nodes.shape[0] * nodes.shape[1] * nodes.shape[2] * xy_scale * xy_scale * z_scale
318
+ densities = {}
319
+ for nodeid, amount in totals.items():
320
+ densities[nodeid] = (neighborhood_dict[nodeid]/search_vol)/(amount/volume)
321
+
322
+ return neighborhood_dict, totals, densities
318
323
 
319
324
 
320
325
 
nettracer3d/nettracer.py CHANGED
@@ -1383,7 +1383,7 @@ def binarize(arrayimage, directory = None):
1383
1383
 
1384
1384
  return arrayimage
1385
1385
 
1386
- def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast_dil = False):
1386
+ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast_dil = False, recursive = False):
1387
1387
  """
1388
1388
  Can be used to dilate a binary image in 3D. Dilated output will be saved to the active directory if none is specified. Note that dilation is done with single-instance kernels and not iterations, and therefore
1389
1389
  objects will lose their shape somewhat and become cube-ish if the 'amount' param is ever significantly larger than the objects in quesiton.
@@ -1408,13 +1408,15 @@ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast
1408
1408
  if len(np.unique(arrayimage)) > 2: #binarize
1409
1409
  arrayimage = binarize(arrayimage)
1410
1410
 
1411
- if not fast_dil:
1411
+ if not fast_dil and not recursive:
1412
1412
  arrayimage = (dilate_3D(arrayimage, dilate_xy, dilate_xy, dilate_z)) * 255
1413
1413
  if np.max(arrayimage) == 1:
1414
1414
  arrayimage = arrayimage * 255
1415
-
1416
- else:
1415
+ elif not recursive:
1417
1416
  arrayimage = (dilate_3D_old(arrayimage, dilate_xy, dilate_xy, dilate_z)) * 255
1417
+ else:
1418
+ arrayimage = (dilate_3D_recursive(arrayimage, dilate_xy, dilate_xy, dilate_z)) * 255
1419
+
1418
1420
 
1419
1421
 
1420
1422
  if type(image) == str:
@@ -1935,6 +1937,9 @@ def encapsulate(parent_dir = None, name = None):
1935
1937
 
1936
1938
  return new_folder_path
1937
1939
 
1940
+
1941
+
1942
+
1938
1943
  #THE 3D NETWORK CLASS
1939
1944
 
1940
1945
  class Network_3D:
@@ -3906,25 +3911,37 @@ class Network_3D:
3906
3911
  return hubs, hub_img
3907
3912
 
3908
3913
 
3909
- def extract_communities(self, color_code = True, down_factor = None):
3914
+ def extract_communities(self, color_code = True, down_factor = None, identities = False):
3910
3915
 
3911
3916
  if down_factor is not None:
3912
3917
  original_shape = self._nodes.shape
3913
3918
  temp = downsample(self._nodes, down_factor)
3914
3919
  if color_code:
3915
- image = community_extractor.assign_community_colors(self.communities, temp)
3920
+ if not identities:
3921
+ image, output = community_extractor.assign_community_colors(self.communities, temp)
3922
+ else:
3923
+ image, output = community_extractor.assign_community_colors(self.node_identities, temp)
3916
3924
  else:
3917
- image = community_extractor.assign_community_grays(self.communities, temp)
3925
+ if not identities:
3926
+ image, output = community_extractor.assign_community_grays(self.communities, temp)
3927
+ else:
3928
+ image, output = community_extractor.assign_community_grays(self.node_identities, temp)
3918
3929
  image = upsample_with_padding(image, down_factor, original_shape)
3919
3930
  else:
3920
3931
 
3921
3932
  if color_code:
3922
- image = community_extractor.assign_community_colors(self.communities, self._nodes)
3933
+ if not identities:
3934
+ image, output = community_extractor.assign_community_colors(self.communities, self._nodes)
3935
+ else:
3936
+ image, output = community_extractor.assign_community_colors(self.node_identities, self._nodes)
3923
3937
  else:
3924
- image = community_extractor.assign_community_grays(self.communities, self._nodes)
3938
+ if not identities:
3939
+ image, output = community_extractor.assign_community_grays(self.communities, self._nodes)
3940
+ else:
3941
+ image, output = community_extractor.assign_community_grays(self.node_identities, self._nodes)
3925
3942
 
3926
3943
 
3927
- return image
3944
+ return image, output
3928
3945
 
3929
3946
 
3930
3947
 
@@ -4072,6 +4089,14 @@ class Network_3D:
4072
4089
  except:
4073
4090
  stats['degree_assortativity'] = "Failed to compute"
4074
4091
 
4092
+ try:
4093
+ nodes = np.unique(self._nodes)
4094
+ if nodes[0] == 0:
4095
+ nodes = np.delete(nodes, 0)
4096
+ stats['Unconnected nodes (left out from node image)'] = (len(nodes) - len(G.nodes()))
4097
+ except:
4098
+ stats['Unconnected nodes (left out from node image)'] = "Failed to compute"
4099
+
4075
4100
 
4076
4101
  return stats
4077
4102
 
@@ -4114,9 +4139,9 @@ class Network_3D:
4114
4139
 
4115
4140
 
4116
4141
  elif mode == 1: #Search neighborhoods morphologically, obtain densities
4117
- neighborhood_dict, total_dict = morphology.search_neighbor_ids(self._nodes, targets, node_identities, neighborhood_dict, total_dict, search, self._xy_scale, self._z_scale)
4118
- title1 = f'Volumetric Neighborhood Distribution of Nodes in image from Nodes: {root}'
4119
- title2 = f'Density Distribution of Nodes in image from Nodes {root} as a proportion of total node volume of that ID'
4142
+ neighborhood_dict, total_dict, densities = morphology.search_neighbor_ids(self._nodes, targets, node_identities, neighborhood_dict, total_dict, search, self._xy_scale, self._z_scale, root)
4143
+ title1 = f'Volumetric Neighborhood Distribution of Nodes in image that are {search} from nodes: {root}'
4144
+ title2 = f'Density Distribution of Nodes in image that are {search} from Nodes {root} as a proportion of total node volume of that ID'
4120
4145
 
4121
4146
 
4122
4147
  for identity in neighborhood_dict:
@@ -4126,11 +4151,13 @@ class Network_3D:
4126
4151
 
4127
4152
  network_analysis.create_bar_graph(proportion_dict, title2, "Node Identity", "Proportion", directory=directory)
4128
4153
 
4154
+ try:
4155
+ network_analysis.create_bar_graph(densities, f'Clustering Factor of Node Identities with {search} from nodes {root}', "Node Identity", "Density Search/Density Total", directory=directory)
4156
+ except:
4157
+ densities = None
4129
4158
 
4130
4159
 
4131
-
4132
-
4133
- return neighborhood_dict, proportion_dict, title1, title2
4160
+ return neighborhood_dict, proportion_dict, title1, title2, densities
4134
4161
 
4135
4162
 
4136
4163
 
@@ -555,6 +555,8 @@ class ImageViewerWindow(QMainWindow):
555
555
  if len(self.clicked_values['nodes']) > 1 or len(self.clicked_values['edges']) > 1:
556
556
  combine_obj = highlight_menu.addAction("Combine Object Labels")
557
557
  combine_obj.triggered.connect(self.handle_combine)
558
+ split_obj = highlight_menu.addAction("Split Non-Touching Labels")
559
+ split_obj.triggered.connect(self.handle_seperate)
558
560
  delete_obj = highlight_menu.addAction("Delete Selection")
559
561
  delete_obj.triggered.connect(self.handle_delete)
560
562
  if len(self.clicked_values['nodes']) > 1:
@@ -991,6 +993,58 @@ class ImageViewerWindow(QMainWindow):
991
993
  except Exception as e:
992
994
  print(f"Error: {e}")
993
995
 
996
+ def handle_info(self, sort = 'node'):
997
+
998
+ try:
999
+
1000
+ info_dict = {}
1001
+
1002
+ if sort == 'node':
1003
+
1004
+ label = self.clicked_values['nodes'][-1]
1005
+
1006
+ info_dict['Label'] = label
1007
+
1008
+ info_dict['Object Class'] = 'Node'
1009
+
1010
+ if my_network.node_identities is not None:
1011
+ info_dict['ID'] = my_network.node_identities[label]
1012
+
1013
+ if my_network.network is not None:
1014
+ info_dict['Degree'] = my_network.network.degree(label)
1015
+
1016
+ if my_network.communities is not None:
1017
+ info_dict['Community'] = my_network.communities[label]
1018
+
1019
+ if my_network.node_centroids is not None:
1020
+ info_dict['Centroid'] = my_network.node_centroids[label]
1021
+
1022
+ if self.volume_dict[0] is not None:
1023
+ info_dict['Volume'] = self.volume_dict[0][label]
1024
+
1025
+
1026
+ elif sort == 'edge':
1027
+
1028
+ label = self.clicked_values['edges'][-1]
1029
+
1030
+ info_dict['Label'] = label
1031
+
1032
+ info_dict['Object Class'] = 'Edge'
1033
+
1034
+ if my_network.edge_centroids is not None:
1035
+ info_dict['Centroid'] = my_network.edge_centroids[label]
1036
+
1037
+ if self.volume_dict[1] is not None:
1038
+ info_dict['Volume'] = self.volume_dict[1][label]
1039
+
1040
+ self.format_for_upperright_table(info_dict, title = f'Info on Object')
1041
+
1042
+ except:
1043
+ pass
1044
+
1045
+
1046
+
1047
+
994
1048
  def handle_combine(self):
995
1049
 
996
1050
  try:
@@ -1040,12 +1094,73 @@ class ImageViewerWindow(QMainWindow):
1040
1094
  for column in range(model.columnCount(None)):
1041
1095
  self.network_table.resizeColumnToContents(column)
1042
1096
 
1097
+ self.highlight_overlay = None
1098
+ self.update_display()
1099
+
1100
+ self.show_centroid_dialog()
1101
+
1043
1102
  except Exception as e:
1044
1103
  print(f"Error, could not update network: {e}")
1045
1104
 
1105
+
1046
1106
  except Exception as e:
1047
1107
  print(f"An error has occured: {e}")
1048
1108
 
1109
+ def handle_seperate(self):
1110
+
1111
+ try:
1112
+
1113
+ if len(self.clicked_values['nodes']) > 0:
1114
+ self.create_highlight_overlay(node_indices = self.clicked_values['nodes'])
1115
+ max_val = np.max(my_network.nodes)
1116
+ self.highlight_overlay, num = n3d.label_objects(self.highlight_overlay)
1117
+
1118
+ node_bools = self.highlight_overlay != 0
1119
+ new_max = num + max_val
1120
+ self.highlight_overlay = self.highlight_overlay + max_val
1121
+ self.highlight_overlay = self.highlight_overlay * node_bools
1122
+ if new_max < 256:
1123
+ dtype = np.uint8
1124
+ elif new_max < 65536:
1125
+ dtype = np.uint16
1126
+ else:
1127
+ dtype = np.uint32
1128
+
1129
+ self.highlight_overlay = self.highlight_overlay.astype(dtype)
1130
+ my_network.nodes = my_network.nodes + self.highlight_overlay
1131
+ self.load_channel(0, my_network.nodes, True)
1132
+
1133
+ if len(self.clicked_values['edges']) > 0:
1134
+ self.create_highlight_overlay(edge_indices = self.clicked_values['edges'])
1135
+ max_val = np.max(my_network.edges)
1136
+ self.highlight_overlay, num = n3d.label_objects(self.highlight_overlay)
1137
+ node_bools = self.highlight_overlay != 0
1138
+ new_max = num + max_val
1139
+
1140
+ self.highlight_overlay = self.highlight_overlay + max_val
1141
+ self.highlight_overlay = self.highlight_overlay * node_bools
1142
+ if new_max < 256:
1143
+ dtype = np.uint8
1144
+ elif new_max < 65536:
1145
+ dtype = np.uint16
1146
+ else:
1147
+ dtype = np.uint32
1148
+
1149
+ self.highlight_overlay = self.highlight_overlay.astype(dtype)
1150
+ my_network.edges = my_network.edges + self.highlight_overlay
1151
+ self.load_channel(1, my_network.edges, True)
1152
+ self.highlight_overlay = None
1153
+ self.update_display()
1154
+ print("Network is not updated automatically, please recompute if necesarry. Identities are not automatically updated.")
1155
+ self.show_centroid_dialog()
1156
+
1157
+ except Exception as e:
1158
+ print(f"Error seperating: {e}")
1159
+
1160
+
1161
+
1162
+
1163
+
1049
1164
  def handle_delete(self):
1050
1165
 
1051
1166
  try:
@@ -1399,6 +1514,7 @@ class ImageViewerWindow(QMainWindow):
1399
1514
  # Try to highlight the last selected value in tables
1400
1515
  if self.clicked_values['edges']:
1401
1516
  self.highlight_value_in_tables(self.clicked_values['edges'][-1])
1517
+
1402
1518
 
1403
1519
  elif not self.selecting and self.selection_start: # If we had a click but never started selection
1404
1520
  # Handle as a normal click
@@ -1564,6 +1680,7 @@ class ImageViewerWindow(QMainWindow):
1564
1680
  self.clicked_values = {'nodes': [clicked_value], 'edges': []}
1565
1681
  # Get latest value (or the last remaining one if we just removed an item)
1566
1682
  latest_value = self.clicked_values['nodes'][-1] if self.clicked_values['nodes'] else None
1683
+ self.handle_info('node')
1567
1684
  elif self.active_channel == 1:
1568
1685
  if ctrl_pressed:
1569
1686
  if clicked_value in self.clicked_values['edges']:
@@ -1577,6 +1694,8 @@ class ImageViewerWindow(QMainWindow):
1577
1694
  self.clicked_values = {'nodes': [], 'edges': [clicked_value]}
1578
1695
  # Get latest value (or the last remaining one if we just removed an item)
1579
1696
  latest_value = self.clicked_values['edges'][-1] if self.clicked_values['edges'] else None
1697
+ self.handle_info('edge')
1698
+
1580
1699
 
1581
1700
  # Try to find and highlight the latest value in the current table
1582
1701
  try:
@@ -1689,7 +1808,9 @@ class ImageViewerWindow(QMainWindow):
1689
1808
  mother_action = overlay_menu.addAction("Get Mother Nodes")
1690
1809
  mother_action.triggered.connect(self.show_mother_dialog)
1691
1810
  community_code_action = overlay_menu.addAction("Code Communities")
1692
- community_code_action.triggered.connect(self.show_code_dialog)
1811
+ community_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Community'))
1812
+ id_code_action = overlay_menu.addAction("Code Identities")
1813
+ id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
1693
1814
 
1694
1815
 
1695
1816
  # Process menu
@@ -2050,6 +2171,37 @@ class ImageViewerWindow(QMainWindow):
2050
2171
  def load_misc(self, sort):
2051
2172
  """Loads various things"""
2052
2173
 
2174
+ def uncork(my_dict, trumper = None):
2175
+
2176
+ if trumper is None:
2177
+ for thing in my_dict:
2178
+ val = my_dict[thing]
2179
+ new_val = val[0]
2180
+ for i in range(1, len(val)):
2181
+ try:
2182
+ new_val += f" AND {val[i]}"
2183
+ except:
2184
+ break
2185
+ my_dict[thing] = new_val
2186
+ elif trumper == '-':
2187
+ for key, value in my_dict.items():
2188
+ my_dict[key] = value[0]
2189
+ else:
2190
+ for thing in my_dict:
2191
+ val = my_dict[thing]
2192
+ if trumper in val:
2193
+ my_dict[thing] = trumper
2194
+ else:
2195
+ new_val = val[0]
2196
+ for i in range(1, len(val)):
2197
+ try:
2198
+ new_val += f" AND {val[i]}"
2199
+ except:
2200
+ break
2201
+ my_dict[thing] = new_val
2202
+
2203
+ return my_dict
2204
+
2053
2205
  if sort != 'Merge Nodes':
2054
2206
 
2055
2207
  try:
@@ -2058,13 +2210,32 @@ class ImageViewerWindow(QMainWindow):
2058
2210
  self,
2059
2211
  f"Load {sort}",
2060
2212
  "",
2061
- "Spreadsheets (*.xlsx *.csv)"
2213
+ "Spreadsheets (*.xlsx *.csv *.json)"
2062
2214
  )
2063
2215
 
2064
2216
  try:
2065
2217
  if sort == 'Node Identities':
2066
2218
  my_network.load_node_identities(file_path = filename)
2067
2219
 
2220
+ first_value = list(my_network.node_identities.values())[0] # Check that there are not multiple IDs
2221
+ if isinstance(first_value, (list, tuple)):
2222
+ trump_value, ok = QInputDialog.getText(
2223
+ self,
2224
+ 'Multiple IDs Detected',
2225
+ 'The node identities appear to contain multiple ids per node in a list.\n'
2226
+ 'If you desire one node ID to trump all others, enter it here.\n'
2227
+ '(Enter "-" to have the first IDs trump all others or press x to skip)'
2228
+ )
2229
+ if not ok or trump_value.strip() == '':
2230
+ trump_value = None
2231
+ elif trump_value.upper() == '-':
2232
+ trump_value = '-'
2233
+ my_network.node_identities = uncork(my_network.node_identities, trump_value)
2234
+ else:
2235
+ trump_value = None
2236
+ my_network.node_identities = uncork(my_network.node_identities, trump_value)
2237
+
2238
+
2068
2239
  if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
2069
2240
  try:
2070
2241
  self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
@@ -2091,9 +2262,13 @@ class ImageViewerWindow(QMainWindow):
2091
2262
 
2092
2263
 
2093
2264
  except Exception as e:
2265
+ import traceback
2266
+ print(traceback.format_exc())
2094
2267
  print(f"An error has occured: {e}")
2095
2268
 
2096
2269
  except Exception as e:
2270
+ import traceback
2271
+ print(traceback.format_exc())
2097
2272
  QMessageBox.critical(
2098
2273
  self,
2099
2274
  "Error Loading",
@@ -2245,7 +2420,7 @@ class ImageViewerWindow(QMainWindow):
2245
2420
  self,
2246
2421
  f"Load Network",
2247
2422
  "",
2248
- "Spreadsheets (*.xlsx *.csv)"
2423
+ "Spreadsheets (*.xlsx *.csv *.json)"
2249
2424
  )
2250
2425
 
2251
2426
  my_network.load_network(file_path = filename)
@@ -2294,7 +2469,6 @@ class ImageViewerWindow(QMainWindow):
2294
2469
  "TIFF Files (*.tif *.tiff)"
2295
2470
  )
2296
2471
  self.channel_data[channel_index] = tifffile.imread(filename)
2297
- print(self.channel_data[channel_index].shape)
2298
2472
  if len(self.channel_data[channel_index].shape) == 2:
2299
2473
  #self.channel_data[channel_index] = np.stack((self.channel_data[channel_index], self.channel_data[channel_index]), axis = 0) #currently handle 2d arrays by just making them 3d
2300
2474
  self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
@@ -2746,8 +2920,8 @@ class ImageViewerWindow(QMainWindow):
2746
2920
  dialog = MotherDialog(self)
2747
2921
  dialog.exec()
2748
2922
 
2749
- def show_code_dialog(self):
2750
- dialog = CodeDialog(self)
2923
+ def show_code_dialog(self, sort = 'Community'):
2924
+ dialog = CodeDialog(self, sort = sort)
2751
2925
  dialog.exec()
2752
2926
 
2753
2927
 
@@ -4134,16 +4308,19 @@ class NeighborIdentityDialog(QDialog):
4134
4308
 
4135
4309
  layout = QFormLayout(self)
4136
4310
 
4137
- self.root = QComboBox()
4138
- self.root.addItems(list(set(my_network.node_identities.values())))
4139
- self.root.setCurrentIndex(0)
4140
- layout.addRow("Root Identity to Search for Neighbor's IDs (search uses nodes of this ID, finds what IDs they connect to", self.root)
4311
+ if my_network.node_identities is not None:
4312
+ self.root = QComboBox()
4313
+ self.root.addItems(list(set(my_network.node_identities.values())))
4314
+ self.root.setCurrentIndex(0)
4315
+ layout.addRow("Root Identity to Search for Neighbor's IDs (search uses nodes of this ID, finds what IDs they connect to", self.root)
4316
+ else:
4317
+ self.root = None
4141
4318
 
4142
4319
  self.directory = QLineEdit("")
4143
4320
  layout.addRow("Output Directory:", self.directory)
4144
4321
 
4145
4322
  self.mode = QComboBox()
4146
- self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Neighborhood Densities"])
4323
+ self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
4147
4324
  self.mode.setCurrentIndex(0)
4148
4325
  layout.addRow("Mode", self.mode)
4149
4326
 
@@ -4157,21 +4334,33 @@ class NeighborIdentityDialog(QDialog):
4157
4334
 
4158
4335
  def neighborids(self):
4159
4336
 
4160
- root = self.root.currentText()
4337
+ try:
4161
4338
 
4162
- directory = self.directory.text() if self.directory.text().strip() else None
4339
+ try:
4340
+ root = self.root.currentText()
4341
+ except:
4342
+ pass
4163
4343
 
4164
- mode = self.mode.currentIndex()
4344
+ directory = self.directory.text() if self.directory.text().strip() else None
4165
4345
 
4166
- search = float(self.search.text()) if self.search.text().strip() else 0
4346
+ mode = self.mode.currentIndex()
4167
4347
 
4348
+ search = float(self.search.text()) if self.search.text().strip() else 0
4168
4349
 
4169
- result, result2, title1, title2 = my_network.neighborhood_identities(root = root, directory = directory, mode = mode, search = search)
4170
4350
 
4171
- self.parent().format_for_upperright_table(result, 'Node Identity', 'Amount', title = title1)
4172
- self.parent().format_for_upperright_table(result2, 'Node Identity', 'Proportion', title = title2)
4351
+ result, result2, title1, title2, densities = my_network.neighborhood_identities(root = root, directory = directory, mode = mode, search = search)
4352
+
4353
+ self.parent().format_for_upperright_table(result, 'Node Identity', 'Amount', title = title1)
4354
+ self.parent().format_for_upperright_table(result2, 'Node Identity', 'Proportion', title = title2)
4355
+
4356
+ if mode == 1:
4357
+
4358
+ self.parent().format_for_upperright_table(densities, 'Node Identity', 'Density in search/density total', title = f'Clustering Factor of Node Identities with {search} from nodes {root}')
4173
4359
 
4174
- self.accept()
4360
+
4361
+ self.accept()
4362
+ except Exception as e:
4363
+ print(f"Error: {e}")
4175
4364
 
4176
4365
 
4177
4366
 
@@ -4521,14 +4710,16 @@ class MotherDialog(QDialog):
4521
4710
 
4522
4711
  class CodeDialog(QDialog):
4523
4712
 
4524
- def __init__(self, parent=None):
4713
+ def __init__(self, parent=None, sort = 'Community'):
4525
4714
 
4526
4715
  super().__init__(parent)
4527
- self.setWindowTitle("Community Code Parameters (Will go to Overlay2)")
4716
+ self.setWindowTitle(f"{sort} Code Parameters (Will go to Overlay2)")
4528
4717
  self.setModal(True)
4529
4718
 
4530
4719
  layout = QFormLayout(self)
4531
4720
 
4721
+ self.sort = sort
4722
+
4532
4723
  self.down_factor = QLineEdit("")
4533
4724
  layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
4534
4725
 
@@ -4540,7 +4731,7 @@ class CodeDialog(QDialog):
4540
4731
 
4541
4732
 
4542
4733
  # Add Run button
4543
- run_button = QPushButton("Community Code")
4734
+ run_button = QPushButton(f"{sort} Code")
4544
4735
  run_button.clicked.connect(self.code)
4545
4736
  layout.addWidget(run_button)
4546
4737
 
@@ -4553,16 +4744,27 @@ class CodeDialog(QDialog):
4553
4744
  down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
4554
4745
 
4555
4746
 
4556
-
4557
- if my_network.communities is None:
4558
- self.parent().show_partition_dialog()
4747
+ if self.sort == 'Community':
4559
4748
  if my_network.communities is None:
4560
- return
4749
+ self.parent().show_partition_dialog()
4750
+ if my_network.communities is None:
4751
+ return
4752
+ elif my_network.node_identities is None:
4753
+ print("Node identities are not set")
4754
+ return
4561
4755
 
4562
- if mode == 0:
4563
- image = my_network.extract_communities(down_factor = down_factor)
4564
- elif mode == 1:
4565
- image = my_network.extract_communities(color_code = False, down_factor = down_factor)
4756
+ if self.sort == 'Community':
4757
+ if mode == 0:
4758
+ image, output = my_network.extract_communities(down_factor = down_factor)
4759
+ elif mode == 1:
4760
+ image, output = my_network.extract_communities(color_code = False, down_factor = down_factor)
4761
+ else:
4762
+ if mode == 0:
4763
+ image, output = my_network.extract_communities(down_factor = down_factor, identities = True)
4764
+ elif mode == 1:
4765
+ image, output = my_network.extract_communities(color_code = False, down_factor = down_factor, identities = True)
4766
+
4767
+ self.parent().format_for_upperright_table(output, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
4566
4768
 
4567
4769
 
4568
4770
  self.parent().load_channel(3, image, True)
@@ -4570,6 +4772,8 @@ class CodeDialog(QDialog):
4570
4772
 
4571
4773
  except Exception as e:
4572
4774
  print(f"An error has occurred: {e}")
4775
+ import traceback
4776
+ print(traceback.format_exc())
4573
4777
 
4574
4778
 
4575
4779
 
@@ -5052,7 +5256,7 @@ class DilateDialog(QDialog):
5052
5256
 
5053
5257
  # Add mode selection dropdown
5054
5258
  self.mode_selector = QComboBox()
5055
- self.mode_selector.addItems(["Binary Dilation", "Preserve Labels (slower)"])
5259
+ self.mode_selector.addItems(["Binary Dilation", "Preserve Labels (slower)", "Recursive Binary Dilation (Use if the dilation radius is much larger than your objects)"])
5056
5260
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5057
5261
  layout.addRow("Execution Mode:", self.mode_selector)
5058
5262
 
@@ -5093,12 +5297,18 @@ class DilateDialog(QDialog):
5093
5297
  self.accept()
5094
5298
  return
5095
5299
 
5300
+ if accepted_mode == 2:
5301
+ recursive = True
5302
+ else:
5303
+ recursive = False
5304
+
5096
5305
  # Call dilate method with parameters
5097
5306
  result = n3d.dilate(
5098
5307
  active_data,
5099
5308
  amount,
5100
5309
  xy_scale = xy_scale,
5101
5310
  z_scale = z_scale,
5311
+ recursive = recursive
5102
5312
  )
5103
5313
 
5104
5314
  # Update both the display data and the network object
@@ -1,5 +1,6 @@
1
1
  import pandas as pd
2
2
  import networkx as nx
3
+ import json
3
4
  import tifffile
4
5
  import numpy as np
5
6
  from networkx.algorithms import community
@@ -105,6 +106,25 @@ def open_network(excel_file_path):
105
106
 
106
107
  def read_excel_to_lists(file_path, sheet_name=0):
107
108
  """Convert a pd dataframe to lists. Handles both .xlsx and .csv files"""
109
+ def load_json_to_list(filename):
110
+ with open(filename, 'r') as f:
111
+ data = json.load(f)
112
+
113
+ # Convert only numeric strings to integers, leave other strings as is
114
+ converted_data = [[],[],[]]
115
+ for i in data[0]:
116
+ try:
117
+ converted_data[0].append(int(data[0][i]))
118
+ converted_data[1].append(int(data[1][i]))
119
+ try:
120
+ converted_data[2].append(int(data[2][i]))
121
+ except IndexError:
122
+ converted_data[2].append(0)
123
+ except ValueError:
124
+ converted_data[k] = v
125
+
126
+ return converted_data
127
+
108
128
  if type(file_path) == str:
109
129
  # Check file extension
110
130
  if file_path.lower().endswith('.xlsx'):
@@ -115,8 +135,11 @@ def read_excel_to_lists(file_path, sheet_name=0):
115
135
  # Read the CSV file into a DataFrame without headers
116
136
  df = pd.read_csv(file_path, header=None)
117
137
  df = df.drop(0)
138
+ elif file_path.lower().endswith('.json'):
139
+ df = load_json_to_list(file_path)
140
+ return df
118
141
  else:
119
- raise ValueError("File must be either .xlsx or .csv format")
142
+ raise ValueError("File must be either .xlsx, .csv, or .json format")
120
143
  else:
121
144
  df = file_path
122
145
 
@@ -134,7 +157,7 @@ def read_excel_to_lists(file_path, sheet_name=0):
134
157
  try:
135
158
  master_list[2].extend(data_lists[i+2])
136
159
  except IndexError:
137
- pass
160
+ master_list[2].extend(0)
138
161
 
139
162
  return master_list
140
163
 
@@ -402,13 +425,28 @@ def read_centroids_to_dict(file_path):
402
425
  Returns:
403
426
  dict: Dictionary with first column as keys and next three columns as numpy array values
404
427
  """
428
+ def load_json_to_dict(filename):
429
+ with open(filename, 'r') as f:
430
+ data = json.load(f)
431
+
432
+ # Convert only numeric strings to integers, leave other strings as is
433
+ converted_data = {}
434
+ for k, v in data.items():
435
+ try:
436
+ converted_data[int(k)] = v
437
+ except ValueError:
438
+ converted_data[k] = v
439
+
440
+ return converted_data
405
441
  # Check file extension
406
442
  if file_path.lower().endswith('.xlsx'):
407
443
  df = pd.read_excel(file_path)
408
444
  elif file_path.lower().endswith('.csv'):
409
445
  df = pd.read_csv(file_path)
446
+ elif file_path.lower().endswith('.json'):
447
+ df = load_json_to_dict(file_path)
410
448
  else:
411
- raise ValueError("Unsupported file format. Please provide either .xlsx or .csv file")
449
+ raise ValueError("Unsupported file format. Please provide either .xlsx, .csv, or .json file")
412
450
 
413
451
  # Initialize an empty dictionary
414
452
  data_dict = {}
@@ -434,13 +472,30 @@ def read_excel_to_singval_dict(file_path):
434
472
  Returns:
435
473
  dict: Dictionary with first column as keys and second column as values
436
474
  """
475
+ def load_json_to_dict(filename):
476
+ with open(filename, 'r') as f:
477
+ data = json.load(f)
478
+
479
+ # Convert only numeric strings to integers, leave other strings as is
480
+ converted_data = {}
481
+ for k, v in data.items():
482
+ try:
483
+ converted_data[int(k)] = v
484
+ except ValueError:
485
+ converted_data[k] = v
486
+
487
+ return converted_data
488
+
437
489
  # Check file extension and read accordingly
438
490
  if file_path.lower().endswith('.xlsx'):
439
491
  df = pd.read_excel(file_path)
440
492
  elif file_path.lower().endswith('.csv'):
441
493
  df = pd.read_csv(file_path)
494
+ elif file_path.lower().endswith('.json'):
495
+ df = load_json_to_dict(file_path)
496
+ return df
442
497
  else:
443
- raise ValueError("Unsupported file format. Please provide either .xlsx or .csv file")
498
+ raise ValueError("Unsupported file format. Please provide either .xlsx, .csv, or .json file")
444
499
 
445
500
  # Convert the DataFrame to a dictionary
446
501
  data_dict = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: nettracer3d
3
- Version: 0.2.7
3
+ Version: 0.2.8
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
@@ -0,0 +1,18 @@
1
+ nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ nettracer3d/community_extractor.py,sha256=o-W0obPAzCgvPB2SAJ3BhPgykevH6GyD8Y5lcd4t3Vw,29725
3
+ nettracer3d/hub_getter.py,sha256=KiNtxdajLkwB1ftslvrh1FE1Ch9ZCFEmHSEEotwR-To,8298
4
+ nettracer3d/modularity.py,sha256=V1f3s_vGd8EuVz27mzq6ycIGr0BWIpH7c7NU4QjgAHU,30247
5
+ nettracer3d/morphology.py,sha256=wv7v06YUcn5lMyefcc_znQlXF5iDxvUdoc0fXOKlGTw,12982
6
+ nettracer3d/nettracer.py,sha256=X33ehVPkfrQHgh4zUTqWlESsZnzs5kc45__llG3UCRc,204438
7
+ nettracer3d/nettracer_gui.py,sha256=aO8gwEgI0pt9rEu1BGNHW9WpSu8xLJohoP7vZ_3Yjnc,281387
8
+ nettracer3d/network_analysis.py,sha256=Nv47U3VcmiTK8b5oEa7rgnYLL4n1r2dF9XCDiQiza4A,45788
9
+ nettracer3d/network_draw.py,sha256=JWWEX7zT6Y9fcO75TtBwkGpPKFIkvBy8pfyB3YB-H_E,12599
10
+ nettracer3d/node_draw.py,sha256=AL8KfFNYBybOx4q6y2pGsAD4QdMebnS-FGRVTqDa0tA,8234
11
+ nettracer3d/proximity.py,sha256=KYs4QUbt1U79RLzTvt8BmrxeGVaeKOQ2brtzTjjA78c,11011
12
+ nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
13
+ nettracer3d/smart_dilate.py,sha256=howfO6Lw5PxNjkaOBSCjkmf7fyau_-_8iTct2mAuTAQ,22083
14
+ nettracer3d-0.2.8.dist-info/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
15
+ nettracer3d-0.2.8.dist-info/METADATA,sha256=spJkNDOCkTGqbv7GPGZc7pletM3c5hfXRpQ79u0GRXA,2258
16
+ nettracer3d-0.2.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
17
+ nettracer3d-0.2.8.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
18
+ nettracer3d-0.2.8.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- nettracer3d/community_extractor.py,sha256=DaDKwb6UuqQWVS0kue4de_HtXxODTvTYRgwZiinEAlU,26632
3
- nettracer3d/hub_getter.py,sha256=KiNtxdajLkwB1ftslvrh1FE1Ch9ZCFEmHSEEotwR-To,8298
4
- nettracer3d/modularity.py,sha256=V1f3s_vGd8EuVz27mzq6ycIGr0BWIpH7c7NU4QjgAHU,30247
5
- nettracer3d/morphology.py,sha256=5cSoLVy0i6NNhHW6s1y4vxQZvY5afKmDbc-kG8Zvh4Q,13204
6
- nettracer3d/nettracer.py,sha256=xS0jv0Vlmr6pyGcgrEk9Hve5MKtGM5GMkTjgQlk-1gc,202853
7
- nettracer3d/nettracer_gui.py,sha256=9i69FuTvWfnA1KYk-wCocWpkvA8_3SJiwX28CgaPC24,272740
8
- nettracer3d/network_analysis.py,sha256=bT9luJ9uRbdw-KhVNeElLAI3MhXP4kuEpwcvCtslPR8,43849
9
- nettracer3d/network_draw.py,sha256=JWWEX7zT6Y9fcO75TtBwkGpPKFIkvBy8pfyB3YB-H_E,12599
10
- nettracer3d/node_draw.py,sha256=AL8KfFNYBybOx4q6y2pGsAD4QdMebnS-FGRVTqDa0tA,8234
11
- nettracer3d/proximity.py,sha256=KYs4QUbt1U79RLzTvt8BmrxeGVaeKOQ2brtzTjjA78c,11011
12
- nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
13
- nettracer3d/smart_dilate.py,sha256=howfO6Lw5PxNjkaOBSCjkmf7fyau_-_8iTct2mAuTAQ,22083
14
- nettracer3d-0.2.7.dist-info/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
15
- nettracer3d-0.2.7.dist-info/METADATA,sha256=jZs7zfReDebDgdZSAG6_oDHLWR-jtBWaLL4rLobjNjk,2258
16
- nettracer3d-0.2.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
17
- nettracer3d-0.2.7.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
18
- nettracer3d-0.2.7.dist-info/RECORD,,