nettracer3d 0.2.8__tar.gz → 0.2.9__tar.gz

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.
Files changed (23) hide show
  1. {nettracer3d-0.2.8/src/nettracer3d.egg-info → nettracer3d-0.2.9}/PKG-INFO +1 -1
  2. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/pyproject.toml +1 -1
  3. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/nettracer.py +26 -18
  4. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/nettracer_gui.py +69 -3
  5. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/network_analysis.py +50 -33
  6. {nettracer3d-0.2.8 → nettracer3d-0.2.9/src/nettracer3d.egg-info}/PKG-INFO +1 -1
  7. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/LICENSE +0 -0
  8. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/README.md +0 -0
  9. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/setup.cfg +0 -0
  10. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/__init__.py +0 -0
  11. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/community_extractor.py +0 -0
  12. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/hub_getter.py +0 -0
  13. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/modularity.py +0 -0
  14. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/morphology.py +0 -0
  15. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/network_draw.py +0 -0
  16. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/node_draw.py +0 -0
  17. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/proximity.py +0 -0
  18. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/simple_network.py +0 -0
  19. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d/smart_dilate.py +0 -0
  20. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  21. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  22. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d.egg-info/requires.txt +0 -0
  23. {nettracer3d-0.2.8 → nettracer3d-0.2.9}/src/nettracer3d.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: nettracer3d
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.2.8"
3
+ version = "0.2.9"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="boom2449@gmail.com" },
6
6
  ]
@@ -322,15 +322,25 @@ def create_and_save_dataframe(pairwise_connections, excel_filename = None):
322
322
  df = pd.concat([df, temp_df], axis=1)
323
323
 
324
324
  if excel_filename is not None:
325
-
326
- try:
325
+ # Remove file extension if present to use as base path
326
+ base_path = excel_filename.rsplit('.', 1)[0]
327
327
 
328
- # Save the DataFrame to an Excel file
329
- df.to_excel(excel_filename, index=False)
330
- print(f"Network file saved to {excel_filename}")
331
-
328
+ # First try to save as CSV
329
+ try:
330
+ csv_path = f"{base_path}.csv"
331
+ df.to_csv(csv_path, index=False)
332
+ print(f"Network file saved to {csv_path}")
333
+ return
332
334
  except Exception as e:
333
- print(f"Unable to write network file to disk... please make sure that {excel_filename} is being saved to a valid directory and try again")
335
+ print(f"Could not save as CSV: {str(e)}")
336
+
337
+ # If CSV fails, try to save as Excel
338
+ try:
339
+ xlsx_path = f"{base_path}.xlsx"
340
+ df.to_excel(xlsx_path, index=False)
341
+ print(f"Network file saved to {xlsx_path}")
342
+ except Exception as e:
343
+ print(f"Unable to write network file to disk... please make sure that {base_path}.xlsx is being saved to a valid directory and try again")
334
344
 
335
345
  else:
336
346
  return df
@@ -2425,7 +2435,7 @@ class Network_3D:
2425
2435
 
2426
2436
 
2427
2437
  if filename is None:
2428
- filename = "drawn_network.tif"
2438
+ filename = "overlay_1.tif"
2429
2439
  elif not filename.endswith(('.tif', '.tiff')):
2430
2440
  filename += '.tif'
2431
2441
 
@@ -2441,7 +2451,7 @@ class Network_3D:
2441
2451
  def save_id_overlay(self, directory = None, filename = None):
2442
2452
 
2443
2453
  if filename is None:
2444
- filename = "labelled_node_indices.tif"
2454
+ filename = "overlay_2.tif"
2445
2455
  if not filename.endswith(('.tif', '.tiff')):
2446
2456
  filename += '.tif'
2447
2457
 
@@ -2462,9 +2472,7 @@ class Network_3D:
2462
2472
  :param directory: (Optional - Val = None; String). The path to an intended directory to save the properties to.
2463
2473
  """
2464
2474
 
2465
-
2466
- if directory is None:
2467
- directory = encapsulate(parent_dir = parent_dir, name = name)
2475
+ directory = encapsulate(parent_dir = parent_dir, name = name)
2468
2476
 
2469
2477
  try:
2470
2478
  self.save_nodes(directory)
@@ -2725,7 +2733,7 @@ class Network_3D:
2725
2733
  items = directory_info(directory)
2726
2734
 
2727
2735
  for item in items:
2728
- if item == 'node_communities.xlsx':
2736
+ if item == 'node_communities.xlsx' or item == 'node_communities.csv':
2729
2737
  if directory is not None:
2730
2738
  self._communities = network_analysis.read_excel_to_singval_dict(f'{directory}/{item}')
2731
2739
  print("Succesfully loaded communities")
@@ -2777,7 +2785,7 @@ class Network_3D:
2777
2785
  items = directory_info(directory)
2778
2786
 
2779
2787
  for item in items:
2780
- if item == 'drawn_network.tif':
2788
+ if item == 'overlay_1.tif':
2781
2789
  if directory is not None:
2782
2790
  self._network_overlay = tifffile.imread(f'{directory}/{item}')
2783
2791
  print("Succesfully loaded network overlay")
@@ -2802,7 +2810,7 @@ class Network_3D:
2802
2810
  items = directory_info(directory)
2803
2811
 
2804
2812
  for item in items:
2805
- if item == 'labelled_node_indices.tif':
2813
+ if item == 'overlay_2.tif':
2806
2814
  if directory is not None:
2807
2815
  self._id_overlay = tifffile.imread(f'{directory}/{item}')
2808
2816
  print("Succesfully loaded id overlay")
@@ -2816,7 +2824,7 @@ class Network_3D:
2816
2824
  #print("Could not find id overlay. They must be in the specified directory and named 'labelled_node_indices.tif'")
2817
2825
 
2818
2826
 
2819
- def assemble(self, directory = None, node_path = None, edge_path = None, search_region_path = None, network_path = None, node_centroids_path = None, node_identities_path = None, edge_centroids_path = None, scaling_path = None, net_overlay_path = None, id_overlay_path = None):
2827
+ def assemble(self, directory = None, node_path = None, edge_path = None, search_region_path = None, network_path = None, node_centroids_path = None, node_identities_path = None, edge_centroids_path = None, scaling_path = None, net_overlay_path = None, id_overlay_path = None, community_path = None ):
2820
2828
  """
2821
2829
  Can be called on a Network_3D object to load all properties simultaneously from a specified directory. It will look for files with the names specified in the property loading methods, in the active directory if none is specified.
2822
2830
  Alternatively, for each property a filepath to any file may be passed to look there to load. This method is intended to be used together with the dump method to easily save and load the Network_3D objects once they had been calculated.
@@ -2840,6 +2848,7 @@ class Network_3D:
2840
2848
  self.load_node_identities(directory, node_identities_path)
2841
2849
  self.load_edge_centroids(directory, edge_centroids_path)
2842
2850
  self.load_scaling(directory, scaling_path)
2851
+ self.load_communities(directory, community_path)
2843
2852
  self.load_network_overlay(directory, net_overlay_path)
2844
2853
  self.load_id_overlay(directory, id_overlay_path)
2845
2854
 
@@ -3132,8 +3141,7 @@ class Network_3D:
3132
3141
  """
3133
3142
 
3134
3143
 
3135
- if directory is None:
3136
- directory = encapsulate()
3144
+ directory = encapsulate()
3137
3145
 
3138
3146
  self._xy_scale = xy_scale
3139
3147
  self._z_scale = z_scale
@@ -2402,6 +2402,13 @@ class ImageViewerWindow(QMainWindow):
2402
2402
  except Exception as e:
2403
2403
  print(f"Error loading node identity table: {e}")
2404
2404
 
2405
+
2406
+ if hasattr(my_network, 'communities') and my_network.communities is not None:
2407
+ try:
2408
+ self.format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
2409
+ except Exception as e:
2410
+ print(f"Error loading node community table: {e}")
2411
+
2405
2412
  except Exception as e:
2406
2413
  QMessageBox.critical(
2407
2414
  self,
@@ -2456,6 +2463,57 @@ class ImageViewerWindow(QMainWindow):
2456
2463
  else:
2457
2464
  btn.setStyleSheet("")
2458
2465
 
2466
+ def reduce_rgb_dimension(self, array, method='first'):
2467
+ """
2468
+ Reduces a 4D array (Z, Y, X, C) to 3D (Z, Y, X) by dropping the color dimension
2469
+ using the specified method.
2470
+
2471
+ Parameters:
2472
+ -----------
2473
+ array : numpy.ndarray
2474
+ 4D array with shape (Z, Y, X, C) where C is the color channel dimension
2475
+ method : str, optional
2476
+ Method to use for reduction:
2477
+ - 'first': takes the first color channel (default)
2478
+ - 'mean': averages across color channels
2479
+ - 'max': takes maximum value across color channels
2480
+ - 'min': takes minimum value across color channels
2481
+
2482
+ Returns:
2483
+ --------
2484
+ numpy.ndarray
2485
+ 3D array with shape (Z, Y, X)
2486
+
2487
+ Raises:
2488
+ -------
2489
+ ValueError
2490
+ If input array is not 4D or method is not recognized
2491
+ """
2492
+ if array.ndim != 4:
2493
+ raise ValueError(f"Expected 4D array, got {array.ndim}D array")
2494
+
2495
+ if method not in ['first', 'mean', 'max', 'min']:
2496
+ raise ValueError(f"Unknown method: {method}")
2497
+
2498
+ if method == 'first':
2499
+ return array[..., 0]
2500
+ elif method == 'mean':
2501
+ return np.mean(array, axis=-1)
2502
+ elif method == 'max':
2503
+ return np.max(array, axis=-1)
2504
+ else: # min
2505
+ return np.min(array, axis=-1)
2506
+
2507
+ def confirm_rgb_dialog(self):
2508
+ """Shows a dialog asking user to confirm if image is 2D RGB"""
2509
+ msg = QMessageBox()
2510
+ msg.setIcon(QMessageBox.Icon.Question)
2511
+ msg.setText("Image Format Detection")
2512
+ msg.setInformativeText("Is this a 2D color (RGB/CMYK) image?")
2513
+ msg.setWindowTitle("Confirm Image Format")
2514
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
2515
+ return msg.exec() == QMessageBox.StandardButton.Yes
2516
+
2459
2517
  def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
2460
2518
  """Load a channel and enable active channel selection if needed."""
2461
2519
 
@@ -2469,14 +2527,22 @@ class ImageViewerWindow(QMainWindow):
2469
2527
  "TIFF Files (*.tif *.tiff)"
2470
2528
  )
2471
2529
  self.channel_data[channel_index] = tifffile.imread(filename)
2472
- if len(self.channel_data[channel_index].shape) == 2:
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
2530
+ if len(self.channel_data[channel_index].shape) == 2: # handle 2d data
2474
2531
  self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
2475
2532
 
2476
-
2477
2533
  else:
2478
2534
  self.channel_data[channel_index] = channel_data
2479
2535
 
2536
+ if len(self.channel_data[channel_index].shape) == 3: # potentially 2D RGB
2537
+ if self.channel_data[channel_index].shape[-1] in (3, 4): # last dim is 3 or 4
2538
+ if self.confirm_rgb_dialog():
2539
+ # User confirmed it's 2D RGB, expand to 4D
2540
+ self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
2541
+
2542
+ if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
2543
+ self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index])
2544
+
2545
+
2480
2546
 
2481
2547
  if channel_index == 0:
2482
2548
  my_network.nodes = self.channel_data[channel_index]
@@ -149,15 +149,15 @@ def read_excel_to_lists(file_path, sheet_name=0):
149
149
  for column_name, column_data in df.items():
150
150
  # Convert the column values to a list and append to the data_lists
151
151
  data_lists.append(column_data.tolist())
152
-
153
152
  master_list = [[], [], []]
154
153
  for i in range(0, len(data_lists), 3):
155
- master_list[0].extend(data_lists[i])
156
- master_list[1].extend(data_lists[i+1])
154
+ master_list[0].extend([int(x) for x in data_lists[i]])
155
+ master_list[1].extend([int(x) for x in data_lists[i+1]])
157
156
  try:
158
- master_list[2].extend(data_lists[i+2])
157
+ master_list[2].extend([int(x) for x in data_lists[i+2]])
159
158
  except IndexError:
160
- master_list[2].extend(0)
159
+ master_list[2].extend([0]) # Note: Changed to list with single int 0
160
+ print(master_list)
161
161
 
162
162
  return master_list
163
163
 
@@ -576,34 +576,35 @@ def find_centroids(nodes, down_factor = None, network = None):
576
576
 
577
577
  return centroid_dict
578
578
 
579
- def _save_centroid_dictionary(centroid_dict, filepath = None, index = 'Node ID'):
579
+ def _save_centroid_dictionary(centroid_dict, filepath=None, index='Node ID'):
580
580
  # Convert dictionary to DataFrame with keys as index and values as a column
581
- #for item in centroid_dict:
582
- #representative = centroid_dict[item]
583
- #break
584
-
585
- #if len(representative) == 3:
586
- #df = pd.DataFrame.from_dict(centroid_dict, orient='index', columns=['Z', 'Y', 'X'])
587
- #elif len(representative) == 2:
588
- #df = pd.DataFrame.from_dict(centroid_dict, orient='index', columns=['Y', 'X'])
589
-
590
581
  df = pd.DataFrame.from_dict(centroid_dict, orient='index', columns=['Z', 'Y', 'X'])
591
-
592
- # Rename the index to 'Node ID'
582
+
583
+ # Rename the index to specified name
593
584
  df.index.name = index
594
-
585
+
595
586
  if filepath is None:
596
- try:
597
- # Save DataFrame to Excel file
598
- df.to_excel('centroids.xlsx', engine='openpyxl')
599
- except Exception as e:
600
- print("Could not save centroids to active directory")
587
+ base_path = 'centroids'
601
588
  else:
589
+ # Remove file extension if present to use as base path
590
+ base_path = filepath.rsplit('.', 1)[0]
591
+
592
+ # First try to save as CSV
593
+ try:
594
+ csv_path = f"{base_path}.csv"
595
+ df.to_csv(csv_path)
596
+ print(f"Successfully saved centroids to {csv_path}")
597
+ return
598
+ except Exception as e:
599
+ print(f"Could not save centroids as CSV: {str(e)}")
600
+
601
+ # If CSV fails, try to save as Excel
602
602
  try:
603
- # Save DataFrame to Excel file
604
- df.to_excel(filepath, engine='openpyxl')
603
+ xlsx_path = f"{base_path}.xlsx"
604
+ df.to_excel(xlsx_path, engine='openpyxl')
605
+ print(f"Successfully saved centroids to {xlsx_path}")
605
606
  except Exception as e:
606
- print(f"Could not save centroids to {filepath}")
607
+ print(f"Could not save centroids as XLSX: {str(e)}")
607
608
 
608
609
  def _find_centroids_GPU(nodes, node_list=None, down_factor=None):
609
610
  """Internal use version to get centroids without saving"""
@@ -1229,15 +1230,31 @@ def edge_to_node(network, node_identities = None):
1229
1230
 
1230
1231
 
1231
1232
  def save_singval_dict(dict, index_name, valname, filename):
1232
-
1233
- #index name goes on the left, valname on the right
1233
+ # Convert dictionary to DataFrame
1234
1234
  df = pd.DataFrame.from_dict(dict, orient='index', columns=[valname])
1235
-
1236
- # Rename the index to 'Node ID'
1235
+
1236
+ # Rename the index
1237
1237
  df.index.name = index_name
1238
-
1239
- # Save DataFrame to Excel file
1240
- df.to_excel(filename, engine='openpyxl')
1238
+
1239
+ # Remove file extension if present to use as base path
1240
+ base_path = filename.rsplit('.', 1)[0]
1241
+
1242
+ # First try to save as CSV
1243
+ try:
1244
+ csv_path = f"{base_path}.csv"
1245
+ df.to_csv(csv_path)
1246
+ print(f"Successfully saved {valname} data to {csv_path}")
1247
+ return
1248
+ except Exception as e:
1249
+ print(f"Could not save as CSV: {str(e)}")
1250
+
1251
+ # If CSV fails, try to save as Excel
1252
+ try:
1253
+ xlsx_path = f"{base_path}.xlsx"
1254
+ df.to_excel(xlsx_path, engine='openpyxl')
1255
+ print(f"Successfully saved {valname} data to {xlsx_path}")
1256
+ except Exception as e:
1257
+ print(f"Could not save as XLSX: {str(e)}")
1241
1258
 
1242
1259
 
1243
1260
  def rand_net_weighted(num_rows, num_nodes, nodes):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: nettracer3d
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
File without changes
File without changes
File without changes