nettracer3d 0.7.7__py3-none-any.whl → 0.7.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

nettracer3d/excelotron.py CHANGED
@@ -1599,7 +1599,13 @@ class ExcelToDictGUI(QMainWindow):
1599
1599
  remapped_data = self.identity_remap_widget.get_remapped_identities(filtered_data)
1600
1600
  result_dict[key_name] = remapped_data
1601
1601
  elif key_name == 'Numerical IDs':
1602
- # Apply same filtering to Numerical IDs
1602
+
1603
+ # Check if user actually dropped a numerical IDs column
1604
+ if widget_id not in self.dict_columns or 'data' not in self.dict_columns[widget_id]:
1605
+ # Auto-generate sequential IDs and assign to column_data
1606
+ column_data = np.array(list(range(1, len(self.df) + 1)))
1607
+
1608
+ # Now use the exact same logic as if user provided the data
1603
1609
  identity_column_data = None
1604
1610
  # Find the identity column data
1605
1611
  for other_widget_id in self.dict_columns:
@@ -1620,11 +1626,55 @@ class ExcelToDictGUI(QMainWindow):
1620
1626
  result_dict[key_name] = filtered_numerical_ids
1621
1627
  else:
1622
1628
  result_dict[key_name] = column_data.tolist()
1629
+
1630
+
1623
1631
  else:
1624
1632
  result_dict[key_name] = column_data.tolist()
1625
1633
  else:
1626
1634
  result_dict[key_name] = column_data.tolist()
1627
1635
  break
1636
+
1637
+ for i in range(self.dict_layout.count()):
1638
+ item = self.dict_layout.itemAt(i)
1639
+ if item and item.widget() and hasattr(item.widget(), 'widget_id'):
1640
+ widget = item.widget()
1641
+ widget_id = widget.widget_id
1642
+ key_name = widget.header_input.text().strip()
1643
+
1644
+ # Skip if already processed (has dropped data) or no key name
1645
+ if widget_id in self.dict_columns or not key_name:
1646
+ continue
1647
+
1648
+ # Handle auto-generation for Node Identities template
1649
+ if property_name == 'Node Identities' and key_name == 'Numerical IDs':
1650
+
1651
+ # Find the identity column data
1652
+ identity_column_data = None
1653
+ for other_widget_id in self.dict_columns:
1654
+ for j in range(self.dict_layout.count()):
1655
+ item_j = self.dict_layout.itemAt(j)
1656
+ if item_j and item_j.widget() and hasattr(item_j.widget(), 'widget_id'):
1657
+ if item_j.widget().widget_id == other_widget_id:
1658
+ other_key_name = item_j.widget().header_input.text().strip()
1659
+ if other_key_name == 'Identity Column':
1660
+ identity_column_data = self.dict_columns[other_widget_id]['data']
1661
+ break
1662
+ if identity_column_data is not None:
1663
+ break
1664
+
1665
+ if identity_column_data is not None:
1666
+ # Auto-generate sequential IDs
1667
+ auto_generated_ids = np.array(list(range(1, len(self.df) + 1)))
1668
+
1669
+ filtered_indices = self.identity_remap_widget.get_filtered_indices(identity_column_data.tolist())
1670
+
1671
+ filtered_numerical_ids = [auto_generated_ids[i] for i in filtered_indices]
1672
+
1673
+ result_dict[key_name] = filtered_numerical_ids
1674
+ else:
1675
+ # Fallback: generate sequential IDs for all rows
1676
+ result_dict[key_name] = list(range(1, len(self.df) + 1))
1677
+
1628
1678
 
1629
1679
  if not result_dict:
1630
1680
  QMessageBox.warning(self, "Warning", "No valid dictionary keys defined")
@@ -202,6 +202,137 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
202
202
 
203
203
  return embedding
204
204
 
205
+ def create_community_heatmap(community_intensity, node_community, node_centroids, is_3d=True,
206
+ figsize=(12, 8), point_size=50, alpha=0.7, colorbar_label="Community Intensity"):
207
+ """
208
+ Create a 2D or 3D heatmap showing nodes colored by their community intensities.
209
+
210
+ Parameters:
211
+ -----------
212
+ community_intensity : dict
213
+ Dictionary mapping community IDs to intensity values
214
+ Keys can be np.int64 or regular ints
215
+
216
+ node_community : dict
217
+ Dictionary mapping node IDs to community IDs
218
+
219
+ node_centroids : dict
220
+ Dictionary mapping node IDs to centroids
221
+ Centroids should be [Z, Y, X] for 3D or [1, Y, X] for pseudo-3D
222
+
223
+ is_3d : bool, default=True
224
+ If True, create 3D plot. If False, create 2D plot.
225
+
226
+ figsize : tuple, default=(12, 8)
227
+ Figure size (width, height)
228
+
229
+ point_size : int, default=50
230
+ Size of scatter plot points
231
+
232
+ alpha : float, default=0.7
233
+ Transparency of points (0-1)
234
+
235
+ colorbar_label : str, default="Community Intensity"
236
+ Label for the colorbar
237
+
238
+ Returns:
239
+ --------
240
+ fig, ax : matplotlib figure and axis objects
241
+ """
242
+
243
+ # Convert numpy int64 keys to regular ints for consistency
244
+ community_intensity_clean = {}
245
+ for k, v in community_intensity.items():
246
+ if hasattr(k, 'item'): # numpy scalar
247
+ community_intensity_clean[k.item()] = v
248
+ else:
249
+ community_intensity_clean[k] = v
250
+
251
+ # Prepare data for plotting
252
+ node_positions = []
253
+ node_intensities = []
254
+
255
+ for node_id, centroid in node_centroids.items():
256
+ try:
257
+ # Get community for this node
258
+ community_id = node_community[node_id]
259
+
260
+ # Convert community_id to regular int if it's numpy
261
+ if hasattr(community_id, 'item'):
262
+ community_id = community_id.item()
263
+
264
+ # Get intensity for this community
265
+ intensity = community_intensity_clean[community_id]
266
+
267
+ node_positions.append(centroid)
268
+ node_intensities.append(intensity)
269
+ except:
270
+ pass
271
+
272
+ # Convert to numpy arrays
273
+ positions = np.array(node_positions)
274
+ intensities = np.array(node_intensities)
275
+
276
+ # Determine min and max intensities for color scaling
277
+ min_intensity = np.min(intensities)
278
+ max_intensity = np.max(intensities)
279
+
280
+ # Create figure
281
+ fig = plt.figure(figsize=figsize)
282
+
283
+ if is_3d:
284
+ # 3D plot
285
+ ax = fig.add_subplot(111, projection='3d')
286
+
287
+ # Extract coordinates (assuming [Z, Y, X] format)
288
+ z_coords = positions[:, 0]
289
+ y_coords = positions[:, 1]
290
+ x_coords = positions[:, 2]
291
+
292
+ # Create scatter plot
293
+ scatter = ax.scatter(x_coords, y_coords, z_coords,
294
+ c=intensities, s=point_size, alpha=alpha,
295
+ cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
296
+
297
+ ax.set_xlabel('X')
298
+ ax.set_ylabel('Y')
299
+ ax.set_zlabel('Z')
300
+ ax.set_title('3D Community Intensity Heatmap')
301
+
302
+ else:
303
+ # 2D plot (using Y, X coordinates, ignoring Z/first dimension)
304
+ ax = fig.add_subplot(111)
305
+
306
+ # Extract Y, X coordinates
307
+ y_coords = positions[:, 1]
308
+ x_coords = positions[:, 2]
309
+
310
+ # Create scatter plot
311
+ scatter = ax.scatter(x_coords, y_coords,
312
+ c=intensities, s=point_size, alpha=alpha,
313
+ cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
314
+
315
+ ax.set_xlabel('X')
316
+ ax.set_ylabel('Y')
317
+ ax.set_title('2D Community Intensity Heatmap')
318
+ ax.grid(True, alpha=0.3)
319
+
320
+ # Set origin to top-left (invert Y-axis)
321
+ ax.invert_yaxis()
322
+
323
+ # Add colorbar
324
+ cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
325
+ cbar.set_label(colorbar_label)
326
+
327
+ # Add text annotations for min/max values
328
+ cbar.ax.text(1.05, 0, f'Min: {min_intensity:.3f}\n(Blue)',
329
+ transform=cbar.ax.transAxes, va='bottom')
330
+ cbar.ax.text(1.05, 1, f'Max: {max_intensity:.3f}\n(Red)',
331
+ transform=cbar.ax.transAxes, va='top')
332
+
333
+ plt.tight_layout()
334
+ plt.show()
335
+
205
336
 
206
337
 
207
338
  # Example usage:
nettracer3d/nettracer.py CHANGED
@@ -3831,6 +3831,41 @@ class Network_3D:
3831
3831
  self._nodes = self._nodes.astype(np.uint16)
3832
3832
 
3833
3833
 
3834
+ def com_by_size(self):
3835
+ """Reassign communities based on size, starting with 1 for largest."""
3836
+
3837
+ from collections import Counter
3838
+
3839
+ # Convert all community values to regular ints (handles numpy scalars)
3840
+ clean_communities = {
3841
+ node: comm.item() if hasattr(comm, 'item') else comm
3842
+ for node, comm in self.communities.items()
3843
+ }
3844
+
3845
+ # Count community sizes and create mapping in one go
3846
+ community_sizes = Counter(clean_communities.values())
3847
+
3848
+ # Create old->new mapping: sort by size (desc), then by community ID for ties
3849
+ old_to_new = {
3850
+ old_comm: new_comm
3851
+ for new_comm, (old_comm, _) in enumerate(
3852
+ sorted(community_sizes.items(), key=lambda x: (-x[1], x[0])),
3853
+ start=1
3854
+ )
3855
+ }
3856
+
3857
+ # Apply mapping
3858
+ self.communities = {
3859
+ node: old_to_new[comm]
3860
+ for node, comm in clean_communities.items()
3861
+ }
3862
+
3863
+
3864
+
3865
+
3866
+
3867
+
3868
+
3834
3869
  def com_to_node(self, targets = None):
3835
3870
 
3836
3871
  def invert_dict(d):
@@ -4982,7 +5017,7 @@ class Network_3D:
4982
5017
 
4983
5018
  return array
4984
5019
 
4985
- def community_cells(self, size = 32):
5020
+ def community_cells(self, size = 32, xy_scale = 1, z_scale = 1):
4986
5021
 
4987
5022
  def invert_dict(d):
4988
5023
  inverted = {}
@@ -4991,10 +5026,61 @@ class Network_3D:
4991
5026
  inverted[value] = key
4992
5027
  return inverted
4993
5028
 
4994
- com_dict = proximity.partition_objects_into_cells(self.node_centroids, size)
5029
+ size_x = int(size * xy_scale)
5030
+ size_z = int(size * z_scale)
5031
+
5032
+ if size_x == size_z:
5033
+
5034
+ com_dict = proximity.partition_objects_into_cells(self.node_centroids, size_x)
5035
+
5036
+ else:
5037
+
5038
+ com_dict = proximity.partition_objects_into_cells(self.node_centroids, (size_z, size_x, size_x))
4995
5039
 
4996
5040
  self.communities = invert_dict(com_dict)
4997
5041
 
5042
+ def community_heatmap(self, num_nodes = None, is3d = True):
5043
+
5044
+ import math
5045
+
5046
+ def invert_dict(d):
5047
+ inverted = {}
5048
+ for key, value in d.items():
5049
+ inverted.setdefault(value, []).append(key)
5050
+ return inverted
5051
+
5052
+ if num_nodes == None:
5053
+
5054
+ try:
5055
+ num_nodes = len(self.network.nodes())
5056
+ except:
5057
+ try:
5058
+ num_nodes = len(self.node_centroids.keys())
5059
+ except:
5060
+ try:
5061
+ num_nodes = len(self.node_identities.keys())
5062
+ except:
5063
+ try:
5064
+ unique = np.unique(self.nodes)
5065
+ num_nodes = len(unique)
5066
+ if unique[0] == 0:
5067
+ num_nodes -= 1
5068
+ except:
5069
+ return
5070
+
5071
+ coms = invert_dict(self.communities)
5072
+
5073
+ rand_dens = num_nodes / len(coms.keys())
5074
+
5075
+ heat_dict = {}
5076
+
5077
+ for com, nodes in coms.items():
5078
+ heat_dict[com] = math.log(len(nodes)/rand_dens)
5079
+
5080
+ from . import neighborhoods
5081
+ neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, is_3d=is3d)
5082
+
5083
+ return heat_dict
4998
5084
 
4999
5085
 
5000
5086
 
@@ -25,7 +25,10 @@ import multiprocessing as mp
25
25
  from concurrent.futures import ThreadPoolExecutor
26
26
  from functools import partial
27
27
  from nettracer3d import segmenter
28
- from nettracer3d import segmenter_GPU
28
+ try:
29
+ from nettracer3d import segmenter_GPU as seg_GPU
30
+ except:
31
+ pass
29
32
  from nettracer3d import excelotron
30
33
 
31
34
 
@@ -205,6 +208,7 @@ class ImageViewerWindow(QMainWindow):
205
208
  self.high_button.setChecked(True)
206
209
  buttons_layout.addWidget(self.high_button)
207
210
  self.highlight = True
211
+ self.needs_mini = False
208
212
 
209
213
  self.pen_button = QPushButton("🖊️")
210
214
  self.pen_button.setCheckable(True)
@@ -394,11 +398,14 @@ class ImageViewerWindow(QMainWindow):
394
398
 
395
399
  #self.canvas.mpl_connect('button_press_event', self.on_mouse_click)
396
400
 
397
- # Initialize measurement points tracking
401
+ # Initialize measurement tracking
398
402
  self.measurement_points = [] # List to store point pairs
399
- self.current_point = None # Store first point of current pair
403
+ self.angle_measurements = [] # NEW: List to store angle trios
404
+ self.current_point = None # Store first point of current pair/trio
405
+ self.current_second_point = None # Store second point when building trio
400
406
  self.current_pair_index = 0 # Track pair numbering
401
-
407
+ self.current_trio_index = 0 # Track trio numbering
408
+ self.measurement_mode = "distance" # "distance" or "angle" mode
402
409
 
403
410
  # Add these new methods for handling neighbors and components (FOR RIGHT CLICKIGN)
404
411
  self.show_neighbors_clicked = None
@@ -664,6 +671,18 @@ class ImageViewerWindow(QMainWindow):
664
671
  edge_indices (list): List of edge indices to highlight
665
672
  """
666
673
 
674
+ if not self.high_button.isChecked():
675
+
676
+ if len(self.clicked_values['edges']) > 0:
677
+ self.format_for_upperright_table(self.clicked_values['edges'], title = 'Selected Edges')
678
+ self.needs_mini = True
679
+ if len(self.clicked_values['nodes']) > 0:
680
+ self.format_for_upperright_table(self.clicked_values['nodes'], title = 'Selected Nodes')
681
+ self.needs_mini = True
682
+
683
+ return
684
+
685
+
667
686
  def process_chunk(chunk_data, indices_to_check):
668
687
  """Process a single chunk of the array to create highlight mask"""
669
688
  mask = np.isin(chunk_data, indices_to_check)
@@ -828,21 +847,38 @@ class ImageViewerWindow(QMainWindow):
828
847
  override_obj.triggered.connect(self.handle_override)
829
848
  context_menu.addMenu(highlight_menu)
830
849
 
831
- # Create measure menu
832
- measure_menu = QMenu("Measure", self)
833
-
850
+ # Create measurement submenu
851
+ measure_menu = context_menu.addMenu("Measurements")
852
+
853
+ # Distance measurement options
854
+ distance_menu = measure_menu.addMenu("Distance")
834
855
  if self.current_point is None:
835
- # If no point is placed, show option to place first point
836
- show_point_menu = measure_menu.addAction("Place Measurement Point")
856
+ show_point_menu = distance_menu.addAction("Place First Point")
837
857
  show_point_menu.triggered.connect(
838
- lambda: self.place_point(x_idx, y_idx, self.current_slice))
858
+ lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
839
859
  else:
840
- # If first point is placed, show option to place second point
841
- show_point_menu = measure_menu.addAction("Place Second Point")
860
+ show_point_menu = distance_menu.addAction("Place Second Point")
842
861
  show_point_menu.triggered.connect(
843
- lambda: self.place_point(x_idx, y_idx, self.current_slice))
844
-
845
- show_remove_menu = measure_menu.addAction("Remove Measurement Points")
862
+ lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
863
+
864
+ # Angle measurement options
865
+ angle_menu = measure_menu.addMenu("Angle")
866
+ if self.current_point is None:
867
+ angle_first = angle_menu.addAction("Place First Point (A)")
868
+ angle_first.triggered.connect(
869
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
870
+ elif self.current_second_point is None:
871
+ angle_second = angle_menu.addAction("Place Second Point (B - Vertex)")
872
+ angle_second.triggered.connect(
873
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
874
+ else:
875
+ angle_third = angle_menu.addAction("Place Third Point (C)")
876
+ angle_third.triggered.connect(
877
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
878
+
879
+ show_remove_menu = measure_menu.addAction("Remove All Measurements")
880
+ show_remove_menu.triggered.connect(self.handle_remove_all_measurements)
881
+
846
882
  context_menu.addMenu(measure_menu)
847
883
 
848
884
  # Connect actions to callbacks
@@ -860,7 +896,6 @@ class ImageViewerWindow(QMainWindow):
860
896
  if self.highlight_overlay is not None or self.mini_overlay_data is not None:
861
897
  highlight_select = context_menu.addAction("Add highlight in network selection")
862
898
  highlight_select.triggered.connect(self.handle_highlight_select)
863
- show_remove_menu.triggered.connect(self.handle_remove_points)
864
899
 
865
900
  cursor_pos = QCursor.pos()
866
901
  context_menu.exec(cursor_pos)
@@ -869,24 +904,25 @@ class ImageViewerWindow(QMainWindow):
869
904
  pass
870
905
 
871
906
 
872
- def place_point(self, x, y, z):
873
- """Place a measurement point at the specified coordinates."""
907
+ def place_distance_point(self, x, y, z):
908
+ """Place a measurement point for distance measurement."""
874
909
  if self.current_point is None:
875
910
  # This is the first point
876
911
  self.current_point = (x, y, z)
877
912
  self.ax.plot(x, y, 'yo', markersize=8)
878
- # Add pair index label above the point
879
- self.ax.text(x, y+5, str(self.current_pair_index),
880
- color='white', ha='center', va='bottom')
913
+ self.ax.text(x, y+5, f"D{self.current_pair_index}",
914
+ color='yellow', ha='center', va='bottom')
881
915
  self.canvas.draw()
882
-
916
+ self.measurement_mode = "distance"
883
917
  else:
884
918
  # This is the second point
885
919
  x1, y1, z1 = self.current_point
886
920
  x2, y2, z2 = x, y, z
887
921
 
888
922
  # Calculate distance
889
- distance = np.sqrt(((x2-x1)*my_network.xy_scale)**2 + ((y2-y1)*my_network.xy_scale)**2 + ((z2-z1)*my_network.z_scale)**2)
923
+ distance = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
924
+ ((y2-y1)*my_network.xy_scale)**2 +
925
+ ((z2-z1)*my_network.z_scale)**2)
890
926
  distance2 = np.sqrt(((x2-x1))**2 + ((y2-y1))**2 + ((z2-z1))**2)
891
927
 
892
928
  # Store the point pair
@@ -900,9 +936,8 @@ class ImageViewerWindow(QMainWindow):
900
936
 
901
937
  # Draw second point and line
902
938
  self.ax.plot(x2, y2, 'yo', markersize=8)
903
- # Add pair index label above the second point
904
- self.ax.text(x2, y2+5, str(self.current_pair_index),
905
- color='white', ha='center', va='bottom')
939
+ self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
940
+ color='yellow', ha='center', va='bottom')
906
941
  if z1 == z2: # Only draw line if points are on same slice
907
942
  self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
908
943
  self.canvas.draw()
@@ -913,46 +948,194 @@ class ImageViewerWindow(QMainWindow):
913
948
  # Reset for next pair
914
949
  self.current_point = None
915
950
  self.current_pair_index += 1
951
+ self.measurement_mode = "distance"
952
+
953
+ def place_angle_point(self, x, y, z):
954
+ """Place a measurement point for angle measurement."""
955
+ if self.current_point is None:
956
+ # First point (A)
957
+ self.current_point = (x, y, z)
958
+ self.ax.plot(x, y, 'go', markersize=8)
959
+ self.ax.text(x, y+5, f"A{self.current_trio_index}",
960
+ color='green', ha='center', va='bottom')
961
+ self.canvas.draw()
962
+ self.measurement_mode = "angle"
963
+
964
+ elif self.current_second_point is None:
965
+ # Second point (B - vertex)
966
+ self.current_second_point = (x, y, z)
967
+ x1, y1, z1 = self.current_point
968
+
969
+ self.ax.plot(x, y, 'go', markersize=8)
970
+ self.ax.text(x, y+5, f"B{self.current_trio_index}",
971
+ color='green', ha='center', va='bottom')
972
+
973
+ # Draw line from A to B
974
+ if z1 == z:
975
+ self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)
976
+ self.canvas.draw()
977
+
978
+ else:
979
+ # Third point (C)
980
+ x1, y1, z1 = self.current_point # Point A
981
+ x2, y2, z2 = self.current_second_point # Point B (vertex)
982
+ x3, y3, z3 = x, y, z # Point C
983
+
984
+ # Calculate angles and distances
985
+ angle_data = self.calculate_3d_angle(
986
+ (x1, y1, z1), (x2, y2, z2), (x3, y3, z3)
987
+ )
988
+
989
+ # Store the trio
990
+ self.angle_measurements.append({
991
+ 'trio_index': self.current_trio_index,
992
+ 'point_a': (x1, y1, z1),
993
+ 'point_b': (x2, y2, z2), # vertex
994
+ 'point_c': (x3, y3, z3),
995
+ **angle_data
996
+ })
997
+
998
+ # Also add the two distances as separate pairs
999
+ dist_ab = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
1000
+ ((y2-y1)*my_network.xy_scale)**2 +
1001
+ ((z2-z1)*my_network.z_scale)**2)
1002
+ dist_bc = np.sqrt(((x3-x2)*my_network.xy_scale)**2 +
1003
+ ((y3-y2)*my_network.xy_scale)**2 +
1004
+ ((z3-z2)*my_network.z_scale)**2)
1005
+
1006
+ dist_ab_voxel = np.sqrt((x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2)
1007
+ dist_bc_voxel = np.sqrt((x3-x2)**2 + (y3-y2)**2 + (z3-z2)**2)
1008
+
1009
+ self.measurement_points.extend([
1010
+ {
1011
+ 'pair_index': f"A{self.current_trio_index}-B{self.current_trio_index}",
1012
+ 'point1': (x1, y1, z1),
1013
+ 'point2': (x2, y2, z2),
1014
+ 'distance': dist_ab,
1015
+ 'distance2': dist_ab_voxel
1016
+ },
1017
+ {
1018
+ 'pair_index': f"B{self.current_trio_index}-C{self.current_trio_index}",
1019
+ 'point1': (x2, y2, z2),
1020
+ 'point2': (x3, y3, z3),
1021
+ 'distance': dist_bc,
1022
+ 'distance2': dist_bc_voxel
1023
+ }
1024
+ ])
1025
+
1026
+ # Draw third point and line
1027
+ self.ax.plot(x3, y3, 'go', markersize=8)
1028
+ self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
1029
+ color='green', ha='center', va='bottom')
1030
+
1031
+ if z2 == z3: # Draw line from B to C if on same slice
1032
+ self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)
1033
+ self.canvas.draw()
1034
+
1035
+ # Update measurement display
1036
+ self.update_measurement_display()
1037
+
1038
+ # Reset for next trio
1039
+ self.current_point = None
1040
+ self.current_second_point = None
1041
+ self.current_trio_index += 1
1042
+ self.measurement_mode = "angle"
1043
+
1044
+ def calculate_3d_angle(self, point_a, point_b, point_c):
1045
+ """Calculate 3D angle at vertex B between points A-B-C."""
1046
+ x1, y1, z1 = point_a
1047
+ x2, y2, z2 = point_b # vertex
1048
+ x3, y3, z3 = point_c
1049
+
1050
+ # Apply scaling
1051
+ scaled_a = np.array([x1 * my_network.xy_scale, y1 * my_network.xy_scale, z1 * my_network.z_scale])
1052
+ scaled_b = np.array([x2 * my_network.xy_scale, y2 * my_network.xy_scale, z2 * my_network.z_scale])
1053
+ scaled_c = np.array([x3 * my_network.xy_scale, y3 * my_network.xy_scale, z3 * my_network.z_scale])
1054
+
1055
+ # Create vectors from vertex B
1056
+ vec_ba = scaled_a - scaled_b
1057
+ vec_bc = scaled_c - scaled_b
1058
+
1059
+ # Calculate angle using dot product
1060
+ dot_product = np.dot(vec_ba, vec_bc)
1061
+ magnitude_ba = np.linalg.norm(vec_ba)
1062
+ magnitude_bc = np.linalg.norm(vec_bc)
1063
+
1064
+ # Avoid division by zero
1065
+ if magnitude_ba == 0 or magnitude_bc == 0:
1066
+ return {'angle_degrees': 0}
1067
+
1068
+ cos_angle = dot_product / (magnitude_ba * magnitude_bc)
1069
+ cos_angle = np.clip(cos_angle, -1.0, 1.0) # Handle numerical errors
1070
+
1071
+ angle_radians = np.arccos(cos_angle)
1072
+ angle_degrees = np.degrees(angle_radians)
1073
+
1074
+ return {'angle_degrees': angle_degrees}
916
1075
 
917
- def handle_remove_points(self):
918
- """Remove all measurement points."""
1076
+ def handle_remove_all_measurements(self):
1077
+ """Remove all measurement points and angles."""
919
1078
  self.measurement_points = []
1079
+ self.angle_measurements = []
920
1080
  self.current_point = None
1081
+ self.current_second_point = None
921
1082
  self.current_pair_index = 0
1083
+ self.current_trio_index = 0
1084
+ self.measurement_mode = "distance"
922
1085
  self.update_display()
923
1086
  self.update_measurement_display()
924
1087
 
925
- # Modify the update_measurement_display method:
926
1088
  def update_measurement_display(self):
927
1089
  """Update the measurement information display in the top right widget."""
1090
+ # Distance measurements
928
1091
  if not self.measurement_points:
929
- # Create empty DataFrame with no specific headers
930
- df = pd.DataFrame()
1092
+ distance_df = pd.DataFrame()
931
1093
  else:
932
- # Create data for DataFrame with measurement-specific headers
933
- data = []
1094
+ distance_data = []
934
1095
  for point in self.measurement_points:
935
1096
  x1, y1, z1 = point['point1']
936
1097
  x2, y2, z2 = point['point2']
937
- data.append({
1098
+ distance_data.append({
938
1099
  'Pair ID': point['pair_index'],
939
1100
  'Point 1 (X,Y,Z)': f"({x1:.1f}, {y1:.1f}, {z1})",
940
1101
  'Point 2 (X,Y,Z)': f"({x2:.1f}, {y2:.1f}, {z2})",
941
1102
  'Scaled Distance': f"{point['distance']:.2f}",
942
1103
  'Voxel Distance': f"{point['distance2']:.2f}"
943
1104
  })
944
- df = pd.DataFrame(data)
1105
+ distance_df = pd.DataFrame(distance_data)
945
1106
 
946
- # Create new table for measurements
947
- table = CustomTableView(self)
948
- table.setModel(PandasModel(df))
1107
+ # Angle measurements
1108
+ if not self.angle_measurements:
1109
+ angle_df = pd.DataFrame()
1110
+ else:
1111
+ angle_data = []
1112
+ for angle in self.angle_measurements:
1113
+ xa, ya, za = angle['point_a']
1114
+ xb, yb, zb = angle['point_b']
1115
+ xc, yc, zc = angle['point_c']
1116
+ angle_data.append({
1117
+ 'Trio ID': f"A{angle['trio_index']}-B{angle['trio_index']}-C{angle['trio_index']}",
1118
+ 'Point A (X,Y,Z)': f"({xa:.1f}, {ya:.1f}, {za})",
1119
+ 'Point B (X,Y,Z)': f"({xb:.1f}, {yb:.1f}, {zb})",
1120
+ 'Point C (X,Y,Z)': f"({xc:.1f}, {yc:.1f}, {zc})",
1121
+ 'Angle (°)': f"{angle['angle_degrees']:.1f}"
1122
+ })
1123
+ angle_df = pd.DataFrame(angle_data)
949
1124
 
950
- # Add to tabbed widget
951
- self.tabbed_data.add_table("Measurements", table)
1125
+ # Create tables
1126
+ if not distance_df.empty:
1127
+ distance_table = CustomTableView(self)
1128
+ distance_table.setModel(PandasModel(distance_df))
1129
+ self.tabbed_data.add_table("Distance Measurements", distance_table)
1130
+ for column in range(distance_table.model().columnCount(None)):
1131
+ distance_table.resizeColumnToContents(column)
952
1132
 
953
- # Adjust column widths to content
954
- for column in range(table.model().columnCount(None)):
955
- table.resizeColumnToContents(column)
1133
+ if not angle_df.empty:
1134
+ angle_table = CustomTableView(self)
1135
+ angle_table.setModel(PandasModel(angle_df))
1136
+ self.tabbed_data.add_table("Angle Measurements", angle_table)
1137
+ for column in range(angle_table.model().columnCount(None)):
1138
+ angle_table.resizeColumnToContents(column)
956
1139
 
957
1140
 
958
1141
  def show_network_table(self):
@@ -1732,6 +1915,12 @@ class ImageViewerWindow(QMainWindow):
1732
1915
  self.highlight = self.high_button.isChecked()
1733
1916
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
1734
1917
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
1918
+
1919
+ if self.high_button.isChecked():
1920
+ if self.highlight_overlay is None and ((len(self.clicked_values['nodes']) + len(self.clicked_values['edges'])) > 0):
1921
+ if self.needs_mini:
1922
+ self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
1923
+ self.needs_mini = False
1735
1924
 
1736
1925
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
1737
1926
 
@@ -2704,6 +2893,8 @@ class ImageViewerWindow(QMainWindow):
2704
2893
  network_menu = analysis_menu.addMenu("Network")
2705
2894
  netshow_action = network_menu.addAction("Show Network")
2706
2895
  netshow_action.triggered.connect(self.show_netshow_dialog)
2896
+ report_action = network_menu.addAction("Generic Network Report")
2897
+ report_action.triggered.connect(self.handle_report)
2707
2898
  partition_action = network_menu.addAction("Community Partition + Generic Community Stats")
2708
2899
  partition_action.triggered.connect(self.show_partition_dialog)
2709
2900
  com_identity_action = network_menu.addAction("Identity Makeup of Network Communities (and UMAP)")
@@ -2723,8 +2914,10 @@ class ImageViewerWindow(QMainWindow):
2723
2914
  degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
2724
2915
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
2725
2916
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
2726
- ripley_action = stats_menu.addAction("Clustering Analysis")
2917
+ ripley_action = stats_menu.addAction("Ripley Clustering Analysis")
2727
2918
  ripley_action.triggered.connect(self.show_ripley_dialog)
2919
+ heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
2920
+ heatmap_action.triggered.connect(self.show_heatmap_dialog)
2728
2921
  vol_action = stats_menu.addAction("Calculate Volumes")
2729
2922
  vol_action.triggered.connect(self.volumes)
2730
2923
  rad_action = stats_menu.addAction("Calculate Radii")
@@ -4396,6 +4589,44 @@ class ImageViewerWindow(QMainWindow):
4396
4589
  dialog = NetShowDialog(self)
4397
4590
  dialog.exec()
4398
4591
 
4592
+ def handle_report(self):
4593
+
4594
+ def invert_dict(d):
4595
+ inverted = {}
4596
+ for key, value in d.items():
4597
+ inverted.setdefault(value, []).append(key)
4598
+ return inverted
4599
+
4600
+ stats = {}
4601
+
4602
+ try:
4603
+ # Basic graph properties
4604
+ stats['num_nodes'] = my_network.network.number_of_nodes()
4605
+ stats['num_edges'] = my_network.network.number_of_edges()
4606
+ except:
4607
+ pass
4608
+
4609
+ try:
4610
+ idens = invert_dict(my_network.node_identities)
4611
+
4612
+ for iden, nodes in idens.items():
4613
+ stats[f'num_nodes_{iden}'] = len(nodes)
4614
+ except:
4615
+ pass
4616
+
4617
+ try:
4618
+
4619
+ coms = invert_dict(my_network.communities)
4620
+
4621
+ for com, nodes in coms.items():
4622
+ stats[f'num_nodes_community_{com}'] = len(nodes)
4623
+ except:
4624
+ pass
4625
+
4626
+ self.format_for_upperright_table(stats, title = 'Network Report')
4627
+
4628
+
4629
+
4399
4630
  def show_partition_dialog(self):
4400
4631
  dialog = PartitionDialog(self)
4401
4632
  dialog.exec()
@@ -4431,6 +4662,10 @@ class ImageViewerWindow(QMainWindow):
4431
4662
  dialog = RipleyDialog(self)
4432
4663
  dialog.exec()
4433
4664
 
4665
+ def show_heatmap_dialog(self):
4666
+ dialog = HeatmapDialog(self)
4667
+ dialog.exec()
4668
+
4434
4669
  def show_random_dialog(self):
4435
4670
  dialog = RandomDialog(self)
4436
4671
  dialog.exec()
@@ -5660,8 +5895,7 @@ class ArbitraryDialog(QDialog):
5660
5895
 
5661
5896
  except Exception as e:
5662
5897
  QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
5663
- import traceback
5664
- print(traceback.format_exc())
5898
+
5665
5899
 
5666
5900
  class Show3dDialog(QDialog):
5667
5901
  def __init__(self, parent=None):
@@ -6223,7 +6457,7 @@ class ComNeighborDialog(QDialog):
6223
6457
  layout.addRow("Min Community Size to be grouped (Smaller communities will be placed in neighborhood 0 - does not apply if empty)", self.limit)
6224
6458
 
6225
6459
  # Add Run button
6226
- run_button = QPushButton("Get Neighborhoods (Note this overwrites current communities - save your coms first)")
6460
+ run_button = QPushButton("Get Communities")
6227
6461
  run_button.clicked.connect(self.run)
6228
6462
  layout.addWidget(run_button)
6229
6463
 
@@ -6277,6 +6511,12 @@ class ComCellDialog(QDialog):
6277
6511
  self.size = QLineEdit("")
6278
6512
  layout.addRow("Cell Size:", self.size)
6279
6513
 
6514
+ self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
6515
+ layout.addRow("xy scale:", self.xy_scale)
6516
+
6517
+ self.z_scale = QLineEdit(f"{my_network.z_scale}")
6518
+ layout.addRow("z scale:", self.z_scale)
6519
+
6280
6520
  # Add Run button
6281
6521
  run_button = QPushButton("Get Neighborhoods (Note this overwrites current communities - save your coms first)")
6282
6522
  run_button.clicked.connect(self.run)
@@ -6286,7 +6526,9 @@ class ComCellDialog(QDialog):
6286
6526
 
6287
6527
  try:
6288
6528
 
6289
- size = int(self.size.text()) if self.size.text().strip() else None
6529
+ size = float(self.size.text()) if self.size.text().strip() else None
6530
+ xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
6531
+ z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
6290
6532
 
6291
6533
  if size is None:
6292
6534
  return
@@ -6296,7 +6538,7 @@ class ComCellDialog(QDialog):
6296
6538
  if my_network.node_centroids is None:
6297
6539
  return
6298
6540
 
6299
- my_network.community_cells(size = size)
6541
+ my_network.community_cells(size = size, xy_scale = xy_scale, z_scale = z_scale)
6300
6542
 
6301
6543
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
6302
6544
 
@@ -6577,6 +6819,60 @@ class RipleyDialog(QDialog):
6577
6819
  print(traceback.format_exc())
6578
6820
  print(f"Error: {e}")
6579
6821
 
6822
+ class HeatmapDialog(QDialog):
6823
+
6824
+ def __init__(self, parent = None):
6825
+
6826
+ super().__init__(parent)
6827
+ self.setWindowTitle("Heatmap Parameters")
6828
+ self.setModal(True)
6829
+
6830
+ layout = QFormLayout(self)
6831
+
6832
+ self.nodecount = QLineEdit("")
6833
+ layout.addRow("(Optional) Total Number of Nodes?:", self.nodecount)
6834
+
6835
+
6836
+ # stats checkbox (default True)
6837
+ self.is3d = QPushButton("3D")
6838
+ self.is3d.setCheckable(True)
6839
+ self.is3d.setChecked(True)
6840
+ layout.addRow("Use 3D Plot (uncheck for 2D)?:", self.is3d)
6841
+
6842
+
6843
+ # Add Run button
6844
+ run_button = QPushButton("Run")
6845
+ run_button.clicked.connect(self.run)
6846
+ layout.addWidget(run_button)
6847
+
6848
+ def run(self):
6849
+
6850
+ nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
6851
+
6852
+ is3d = self.is3d.isChecked()
6853
+
6854
+
6855
+ if my_network.communities is None:
6856
+ if my_network.network is not None:
6857
+ self.parent().show_partition_dialog()
6858
+ else:
6859
+ self.parent().handle_com_cell()
6860
+ if my_network.communities is None:
6861
+ return
6862
+
6863
+ heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
6864
+
6865
+ self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
6866
+
6867
+ self.accept()
6868
+
6869
+
6870
+
6871
+
6872
+
6873
+
6874
+
6875
+
6580
6876
  class RandomDialog(QDialog):
6581
6877
 
6582
6878
  def __init__(self, parent=None):
@@ -7997,7 +8293,7 @@ class MachineWindow(QMainWindow):
7997
8293
  if not GPU:
7998
8294
  self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
7999
8295
  else:
8000
- self.segmenter = segmenter_GPU.InteractiveSegmenter(active_data)
8296
+ self.segmenter = seg_GPU.InteractiveSegmenter(active_data)
8001
8297
 
8002
8298
  self.segmentation_worker = None
8003
8299
 
@@ -8103,7 +8399,7 @@ class MachineWindow(QMainWindow):
8103
8399
  if self.GPU.isChecked():
8104
8400
 
8105
8401
  try:
8106
- self.segmenter = segmenter_GPU.InteractiveSegmenter(active_data)
8402
+ self.segmenter = seg_GPU.InteractiveSegmenter(active_data)
8107
8403
  print("Using GPU")
8108
8404
  except:
8109
8405
  self.GPU.setChecked(False)
@@ -10361,6 +10657,12 @@ class ModifyDialog(QDialog):
10361
10657
  self.isolate.setChecked(False)
10362
10658
  layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
10363
10659
 
10660
+ # isolate checkbox (default false)
10661
+ self.com_sizes = QPushButton("Communities By Size")
10662
+ self.com_sizes.setCheckable(True)
10663
+ self.com_sizes.setChecked(False)
10664
+ layout.addRow("Rearrange Community IDs by size?:", self.com_sizes)
10665
+
10364
10666
  # Community collapse checkbox (default False)
10365
10667
  self.comcollapse = QPushButton("Communities -> nodes")
10366
10668
  self.comcollapse.setCheckable(True)
@@ -10403,6 +10705,7 @@ class ModifyDialog(QDialog):
10403
10705
  isolate = self.isolate.isChecked()
10404
10706
  comcollapse = self.comcollapse.isChecked()
10405
10707
  remove = self.remove.isChecked()
10708
+ com_size = self.com_sizes.isChecked()
10406
10709
 
10407
10710
 
10408
10711
  if isolate and my_network.node_identities is not None:
@@ -10453,6 +10756,14 @@ class ModifyDialog(QDialog):
10453
10756
  self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
10454
10757
  except:
10455
10758
  pass
10759
+ if com_size:
10760
+ if my_network.communities is None:
10761
+ self.parent().show_partition_dialog()
10762
+ if my_network.communities is None:
10763
+ return
10764
+ my_network.com_by_size()
10765
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
10766
+
10456
10767
  if comcollapse:
10457
10768
  if my_network.communities is None:
10458
10769
  self.parent().show_partition_dialog()
@@ -10482,6 +10793,8 @@ class ModifyDialog(QDialog):
10482
10793
  self.accept()
10483
10794
 
10484
10795
  except Exception as e:
10796
+ import traceback
10797
+ print(traceback.format_exc())
10485
10798
  print(f"An error occurred: {e}")
10486
10799
 
10487
10800
 
@@ -10612,85 +10925,107 @@ class CalcAllDialog(QDialog):
10612
10925
  prev_fastdil = False
10613
10926
  prev_overlays = False
10614
10927
  prev_updates = True
10615
-
10928
+
10616
10929
  def __init__(self, parent=None):
10617
10930
  super().__init__(parent)
10618
- self.setWindowTitle("Calculate All Parameters")
10931
+ self.setWindowTitle("Calculate Connectivity Network Parameters")
10619
10932
  self.setModal(True)
10620
10933
 
10621
- layout = QFormLayout(self)
10934
+ # Main layout
10935
+ main_layout = QVBoxLayout(self)
10622
10936
 
10623
- # Directory (empty by default)
10624
- self.directory = QLineEdit(self.prev_directory)
10625
- self.directory.setPlaceholderText("Leave empty for 'my_network'")
10626
- layout.addRow("Output Directory:", self.directory)
10937
+ # Important Parameters Group
10938
+ important_group = QGroupBox("Important Parameters")
10939
+ important_layout = QFormLayout(important_group)
10627
10940
 
10628
- # Load previous values for all inputs
10629
10941
  self.xy_scale = QLineEdit(f'{my_network.xy_scale}')
10630
- layout.addRow("xy_scale:", self.xy_scale)
10942
+ important_layout.addRow("xy_scale:", self.xy_scale)
10631
10943
 
10632
10944
  self.z_scale = QLineEdit(f'{my_network.z_scale}')
10633
- layout.addRow("z_scale:", self.z_scale)
10634
-
10945
+ important_layout.addRow("z_scale:", self.z_scale)
10946
+
10635
10947
  self.search = QLineEdit(self.prev_search)
10636
10948
  self.search.setPlaceholderText("Leave empty for None")
10637
- layout.addRow("Node Search (float):", self.search)
10638
-
10949
+ important_layout.addRow("Node Search (float):", self.search)
10950
+
10639
10951
  self.diledge = QLineEdit(self.prev_diledge)
10640
10952
  self.diledge.setPlaceholderText("Leave empty for None")
10641
- layout.addRow("Edge Reconnection Distance (float):", self.diledge)
10642
-
10643
- self.down_factor = QLineEdit(self.prev_down_factor)
10644
- self.down_factor.setPlaceholderText("Leave empty for None")
10645
- layout.addRow("Downsample for Centroids (int):", self.down_factor)
10646
-
10647
- self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
10648
- self.GPU_downsample.setPlaceholderText("Leave empty for None")
10649
- layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
10650
-
10953
+ important_layout.addRow("Edge Reconnection Distance (float):", self.diledge)
10954
+
10955
+ self.label_nodes = QPushButton("Label")
10956
+ self.label_nodes.setCheckable(True)
10957
+ self.label_nodes.setChecked(self.prev_label_nodes)
10958
+ important_layout.addRow("Re-Label Nodes (WARNING - OVERRIDES ANY CURRENT LABELS):", self.label_nodes)
10959
+
10960
+ main_layout.addWidget(important_group)
10961
+
10962
+ # Optional Parameters Group
10963
+ optional_group = QGroupBox("Optional Parameters")
10964
+ optional_layout = QFormLayout(optional_group)
10965
+
10651
10966
  self.other_nodes = QLineEdit(self.prev_other_nodes)
10652
10967
  self.other_nodes.setPlaceholderText("Leave empty for None")
10653
- layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
10654
-
10968
+ optional_layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
10969
+
10655
10970
  self.remove_trunk = QLineEdit(self.prev_remove_trunk)
10656
10971
  self.remove_trunk.setPlaceholderText("Leave empty for 0")
10657
- layout.addRow("Times to remove edge trunks (int): ", self.remove_trunk)
10658
-
10659
- # Load previous button states
10660
- self.gpu = QPushButton("GPU")
10661
- self.gpu.setCheckable(True)
10662
- self.gpu.setChecked(self.prev_gpu)
10663
- layout.addRow("Use GPU:", self.gpu)
10664
-
10665
- self.label_nodes = QPushButton("Label")
10666
- self.label_nodes.setCheckable(True)
10667
- self.label_nodes.setChecked(self.prev_label_nodes)
10668
- layout.addRow("Re-Label Nodes (WARNING - OVERRIDES ANY CURRENT LABELS):", self.label_nodes)
10669
-
10972
+ optional_layout.addRow("Times to remove edge trunks (int):", self.remove_trunk)
10973
+
10670
10974
  self.inners = QPushButton("Inner Edges")
10671
10975
  self.inners.setCheckable(True)
10672
10976
  self.inners.setChecked(self.prev_inners)
10673
- layout.addRow("Use Inner Edges:", self.inners)
10674
-
10977
+ optional_layout.addRow("Use Inner Edges:", self.inners)
10978
+
10979
+ main_layout.addWidget(optional_group)
10980
+
10981
+ # Speed Up Options Group
10982
+ speedup_group = QGroupBox("Speed Up Options")
10983
+ speedup_layout = QFormLayout(speedup_group)
10984
+
10985
+ self.down_factor = QLineEdit(self.prev_down_factor)
10986
+ self.down_factor.setPlaceholderText("Leave empty for None")
10987
+ speedup_layout.addRow("Downsample for Centroids (int):", self.down_factor)
10988
+
10989
+ self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
10990
+ self.GPU_downsample.setPlaceholderText("Leave empty for None")
10991
+ speedup_layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
10992
+
10993
+ self.gpu = QPushButton("GPU")
10994
+ self.gpu.setCheckable(True)
10995
+ self.gpu.setChecked(self.prev_gpu)
10996
+ speedup_layout.addRow("Use GPU:", self.gpu)
10997
+
10675
10998
  self.fastdil = QPushButton("Fast Dilate")
10676
10999
  self.fastdil.setCheckable(True)
10677
11000
  self.fastdil.setChecked(self.prev_fastdil)
10678
- layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10679
-
11001
+ speedup_layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
11002
+
11003
+ main_layout.addWidget(speedup_group)
11004
+
11005
+ # Output Options Group
11006
+ output_group = QGroupBox("Output Options")
11007
+ output_layout = QFormLayout(output_group)
11008
+
11009
+ self.directory = QLineEdit(self.prev_directory)
11010
+ self.directory.setPlaceholderText("Will Have to Save Manually If Empty")
11011
+ output_layout.addRow("Output Directory:", self.directory)
11012
+
10680
11013
  self.overlays = QPushButton("Overlays")
10681
11014
  self.overlays.setCheckable(True)
10682
11015
  self.overlays.setChecked(self.prev_overlays)
10683
- layout.addRow("Generate Overlays:", self.overlays)
10684
-
11016
+ output_layout.addRow("Generate Overlays:", self.overlays)
11017
+
10685
11018
  self.update = QPushButton("Update")
10686
11019
  self.update.setCheckable(True)
10687
11020
  self.update.setChecked(self.prev_updates)
10688
- layout.addRow("Update Node/Edge in NetTracer3D:", self.update)
11021
+ output_layout.addRow("Update Node/Edge in NetTracer3D:", self.update)
11022
+
11023
+ main_layout.addWidget(output_group)
10689
11024
 
10690
11025
  # Add Run button
10691
11026
  run_button = QPushButton("Run Calculate All")
10692
11027
  run_button.clicked.connect(self.run_calc_all)
10693
- layout.addRow(run_button)
11028
+ main_layout.addWidget(run_button)
10694
11029
 
10695
11030
  def run_calc_all(self):
10696
11031
 
@@ -10867,65 +11202,87 @@ class CalcAllDialog(QDialog):
10867
11202
  f"Error running calculate all: {str(e)}"
10868
11203
  )
10869
11204
 
10870
- class ProxDialog(QDialog):
10871
11205
 
11206
+ class ProxDialog(QDialog):
10872
11207
  def __init__(self, parent=None):
10873
11208
  super().__init__(parent)
10874
11209
  self.setWindowTitle("Calculate Proximity Network")
10875
11210
  self.setModal(True)
10876
-
10877
- layout = QFormLayout(self)
10878
-
10879
- # Directory (empty by default)
10880
- self.directory = QLineEdit('')
10881
- self.directory.setPlaceholderText("Leave empty for 'my_network'")
10882
- layout.addRow("Output Directory:", self.directory)
10883
-
11211
+
11212
+ # Main layout
11213
+ main_layout = QVBoxLayout(self)
11214
+
11215
+ # Important Parameters Group
11216
+ important_group = QGroupBox("Important Parameters")
11217
+ important_layout = QFormLayout(important_group)
11218
+
10884
11219
  self.search = QLineEdit()
10885
11220
  self.search.setPlaceholderText("search")
10886
- layout.addRow("Search Region Distance? (enter true value corresponding to scaling, ie in microns):", self.search)
10887
-
10888
- # Load previous values for all inputs
11221
+ important_layout.addRow("Search Region Distance? (enter true value corresponding to scaling, ie in microns):", self.search)
11222
+
10889
11223
  self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
10890
- layout.addRow("xy_scale:", self.xy_scale)
11224
+ important_layout.addRow("xy_scale:", self.xy_scale)
10891
11225
 
10892
11226
  self.z_scale = QLineEdit(f"{my_network.z_scale}")
10893
- layout.addRow("z_scale:", self.z_scale)
10894
-
10895
- # Add mode selection dropdown
11227
+ important_layout.addRow("z_scale:", self.z_scale)
11228
+
11229
+ main_layout.addWidget(important_group)
11230
+
11231
+ # Mode Group
11232
+ mode_group = QGroupBox("Mode")
11233
+ mode_layout = QFormLayout(mode_group)
11234
+
10896
11235
  self.mode_selector = QComboBox()
10897
11236
  self.mode_selector.addItems(["From Centroids (fast but ignores shape - use for small or spherical objects - search STARTS at centroid)", "From Morphological Shape (slower but preserves shape - use for oddly shaped objects - search STARTS at object border)"])
10898
11237
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
10899
- layout.addRow("Execution Mode:", self.mode_selector)
10900
-
10901
- self.fastdil = QPushButton("Fast Dilate")
10902
- self.fastdil.setCheckable(True)
10903
- self.fastdil.setChecked(False)
10904
- layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10905
-
11238
+ mode_layout.addRow("Execution Mode:", self.mode_selector)
11239
+
10906
11240
  if my_network.node_identities is not None:
10907
11241
  self.id_selector = QComboBox()
10908
11242
  # Add all options from id dictionary
10909
11243
  self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
10910
11244
  self.id_selector.setCurrentIndex(0) # Default to Mode 1
10911
- layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
11245
+ mode_layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
10912
11246
  else:
10913
11247
  self.id_selector = None
10914
-
11248
+
11249
+ main_layout.addWidget(mode_group)
11250
+
11251
+ # Output Options Group
11252
+ output_group = QGroupBox("Output Options")
11253
+ output_layout = QFormLayout(output_group)
11254
+
11255
+ self.directory = QLineEdit('')
11256
+ self.directory.setPlaceholderText("Leave empty for 'my_network'")
11257
+ output_layout.addRow("Output Directory:", self.directory)
11258
+
10915
11259
  self.overlays = QPushButton("Overlays")
10916
11260
  self.overlays.setCheckable(True)
10917
11261
  self.overlays.setChecked(True)
10918
- layout.addRow("Generate Overlays:", self.overlays)
10919
-
11262
+ output_layout.addRow("Generate Overlays:", self.overlays)
11263
+
10920
11264
  self.populate = QPushButton("Populate Nodes from Centroids?")
10921
11265
  self.populate.setCheckable(True)
10922
11266
  self.populate.setChecked(False)
10923
- layout.addRow("If using centroid search:", self.populate)
10924
-
11267
+ output_layout.addRow("If using centroid search:", self.populate)
11268
+
11269
+ main_layout.addWidget(output_group)
11270
+
11271
+ # Speed Up Options Group
11272
+ speedup_group = QGroupBox("Speed Up Options")
11273
+ speedup_layout = QFormLayout(speedup_group)
11274
+
11275
+ self.fastdil = QPushButton("Fast Dilate")
11276
+ self.fastdil.setCheckable(True)
11277
+ self.fastdil.setChecked(False)
11278
+ speedup_layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
11279
+
11280
+ main_layout.addWidget(speedup_group)
11281
+
10925
11282
  # Add Run button
10926
11283
  run_button = QPushButton("Run Proximity Network")
10927
11284
  run_button.clicked.connect(self.prox)
10928
- layout.addRow(run_button)
11285
+ main_layout.addWidget(run_button)
10929
11286
 
10930
11287
  def prox(self):
10931
11288
 
nettracer3d/proximity.py CHANGED
@@ -723,7 +723,7 @@ def partition_objects_into_cells(object_centroids, cell_size):
723
723
  cell_indices[1] * num_cells[2] +
724
724
  cell_indices[2])
725
725
 
726
- cell_assignments[int(cell_number)].append(label)
726
+ cell_assignments[int(cell_number)].append(int(label))
727
727
 
728
728
  # Convert defaultdict to regular dict and sort keys
729
729
  return dict(sorted(cell_assignments.items()))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.7.7
3
+ Version: 0.7.9
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <liamm@wustl.edu>
6
6
  Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
@@ -73,6 +73,6 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
73
73
 
74
74
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
75
75
 
76
- -- Version 0.7.7 Updates --
76
+ -- Version 0.7.9 Updates --
77
77
 
78
- * See documentation once updated
78
+ * The GPU segmenter was being imported regardless of GPU status, causing the program to fail without cupy (which should be optional), fixed that.
@@ -1,23 +1,23 @@
1
1
  nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  nettracer3d/community_extractor.py,sha256=BrONCLRLYUdMfLe_018AQi0k0J7xwVahc_lmsOO5Pwo,23086
3
- nettracer3d/excelotron.py,sha256=jTAwMKIvyLgQBhDSHAhlTH02c9rQAKMck4ic5lE2KM4,68224
3
+ nettracer3d/excelotron.py,sha256=lS5vnpoOGZWp7fdqVpTPqeC-mUKrfwDrWHfx4PQ7Uzg,71384
4
4
  nettracer3d/modularity.py,sha256=O9OeKbjD3v6gSFz9K2GzP6LsxlpQaPfeJbM1pyIEigw,21788
5
5
  nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
6
- nettracer3d/neighborhoods.py,sha256=oCnvfGSjWdpPUdzUn_WhQVSIptGIYSVI0rmzWP_CfE0,7795
7
- nettracer3d/nettracer.py,sha256=RR3UFReS86AHQXHBZkzwQfhtA8pFtDK9K41pvxY5lbg,223147
8
- nettracer3d/nettracer_gui.py,sha256=8ZaReS5KbY4hgIWn65FHkBlTyrWwwowbsr09DATq3g4,454229
6
+ nettracer3d/neighborhoods.py,sha256=kkKR8m6Gjw34cDd_mytAIwLxqvuNBtQb2hU4JuBY9pI,12301
7
+ nettracer3d/nettracer.py,sha256=M1KFIPg7WCzm8BXQOreuEVhgjg0PpLKRg4Y88DyVuK8,225843
8
+ nettracer3d/nettracer_gui.py,sha256=wdx_9putVHl0H4NIf_Bnmj0epgDb7OEMZYXb8pD8cYo,468815
9
9
  nettracer3d/network_analysis.py,sha256=h-5yzUWdE0hcWYy8wcBA5LV1bRhdqiMnKbQLrRzb1Sw,41443
10
10
  nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
11
11
  nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
12
- nettracer3d/proximity.py,sha256=A81hNug8G4Rxx8OizhhzcnAP2ozoZghl52q3Kgefw3I,27991
12
+ nettracer3d/proximity.py,sha256=5n8qxqxmmMtq5bqVpSkqw3EefuZIyGdLybVs18D3ZNg,27996
13
13
  nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
14
14
  nettracer3d/segmenter.py,sha256=BD9vxnblDKXfmR8hLP_iVqqfVngH6opz4Q7V6sxc2KM,60062
15
15
  nettracer3d/segmenter_GPU.py,sha256=Fqr0Za6X2ss4rfaziqOhvhBfbGDPHkHw6fVxs39lZaU,53862
16
16
  nettracer3d/simple_network.py,sha256=Ft_81VhVQ3rqoXvuYnsckXuxCcQSJfakhOfkFaclxZY,9340
17
17
  nettracer3d/smart_dilate.py,sha256=DOEOQq9ig6-AO4MpqAG0CqrGDFqw5_UBeqfSedqHk28,25933
18
- nettracer3d-0.7.7.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
19
- nettracer3d-0.7.7.dist-info/METADATA,sha256=VvJgZUXLw2U4nMlLO2fuMvEc9Pt3EVaeS3jrXpC7e5c,4141
20
- nettracer3d-0.7.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- nettracer3d-0.7.7.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
22
- nettracer3d-0.7.7.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
23
- nettracer3d-0.7.7.dist-info/RECORD,,
18
+ nettracer3d-0.7.9.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
19
+ nettracer3d-0.7.9.dist-info/METADATA,sha256=3EyPBVXOMeZqA9D4vTpBvhOo6wOj_5KHW5lacpuApEk,4254
20
+ nettracer3d-0.7.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ nettracer3d-0.7.9.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
22
+ nettracer3d-0.7.9.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
23
+ nettracer3d-0.7.9.dist-info/RECORD,,