nettracer3d 0.2.8__tar.gz → 0.3.0__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.3.0}/PKG-INFO +1 -1
  2. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/pyproject.toml +1 -1
  3. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/nettracer.py +45 -31
  4. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/nettracer_gui.py +69 -3
  5. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/network_analysis.py +49 -33
  6. {nettracer3d-0.2.8 → nettracer3d-0.3.0/src/nettracer3d.egg-info}/PKG-INFO +1 -1
  7. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/LICENSE +0 -0
  8. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/README.md +0 -0
  9. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/setup.cfg +0 -0
  10. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/__init__.py +0 -0
  11. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/community_extractor.py +0 -0
  12. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/hub_getter.py +0 -0
  13. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/modularity.py +0 -0
  14. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/morphology.py +0 -0
  15. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/network_draw.py +0 -0
  16. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/node_draw.py +0 -0
  17. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/proximity.py +0 -0
  18. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/simple_network.py +0 -0
  19. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d/smart_dilate.py +0 -0
  20. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  21. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  22. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/src/nettracer3d.egg-info/requires.txt +0 -0
  23. {nettracer3d-0.2.8 → nettracer3d-0.3.0}/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.3.0
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.3.0"
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
@@ -686,29 +696,35 @@ def fill_holes_3d(array):
686
696
 
687
697
  array = binarize(array)
688
698
  inv_array = invert_array(array)
699
+
689
700
 
690
701
  # Create arrays for all three planes
691
702
  array_xy = np.zeros_like(inv_array, dtype=np.uint8)
692
703
  array_xz = np.zeros_like(inv_array, dtype=np.uint8)
693
704
  array_yz = np.zeros_like(inv_array, dtype=np.uint8)
694
-
705
+
706
+
695
707
  # Process XY plane
696
708
  for z in range(inv_array.shape[0]):
697
709
  array_xy[z] = process_slice(inv_array[z])
710
+
711
+ if array.shape[0] > 3: #only use these dimensions for sufficiently large zstacks
698
712
 
699
- # Process XZ plane
700
- for y in range(inv_array.shape[1]):
701
- slice_xz = inv_array[:, y, :]
702
- array_xz[:, y, :] = process_slice(slice_xz)
713
+ # Process XZ plane
714
+ for y in range(inv_array.shape[1]):
715
+ slice_xz = inv_array[:, y, :]
716
+ array_xz[:, y, :] = process_slice(slice_xz)
717
+
718
+ # Process YZ plane
719
+ for x in range(inv_array.shape[2]):
720
+ slice_yz = inv_array[:, :, x]
721
+ array_yz[:, :, x] = process_slice(slice_yz)
703
722
 
704
- # Process YZ plane
705
- for x in range(inv_array.shape[2]):
706
- slice_yz = inv_array[:, :, x]
707
- array_yz[:, :, x] = process_slice(slice_yz)
708
-
709
- # Combine results from all three planes
710
- filled = (array_xy | array_xz | array_yz) * 255
711
- return array + filled
723
+ # Combine results from all three planes
724
+ filled = (array_xy | array_xz | array_yz) * 255
725
+ return array + filled
726
+ else:
727
+ return array_xy * 255
712
728
 
713
729
 
714
730
 
@@ -2425,7 +2441,7 @@ class Network_3D:
2425
2441
 
2426
2442
 
2427
2443
  if filename is None:
2428
- filename = "drawn_network.tif"
2444
+ filename = "overlay_1.tif"
2429
2445
  elif not filename.endswith(('.tif', '.tiff')):
2430
2446
  filename += '.tif'
2431
2447
 
@@ -2441,7 +2457,7 @@ class Network_3D:
2441
2457
  def save_id_overlay(self, directory = None, filename = None):
2442
2458
 
2443
2459
  if filename is None:
2444
- filename = "labelled_node_indices.tif"
2460
+ filename = "overlay_2.tif"
2445
2461
  if not filename.endswith(('.tif', '.tiff')):
2446
2462
  filename += '.tif'
2447
2463
 
@@ -2462,9 +2478,7 @@ class Network_3D:
2462
2478
  :param directory: (Optional - Val = None; String). The path to an intended directory to save the properties to.
2463
2479
  """
2464
2480
 
2465
-
2466
- if directory is None:
2467
- directory = encapsulate(parent_dir = parent_dir, name = name)
2481
+ directory = encapsulate(parent_dir = parent_dir, name = name)
2468
2482
 
2469
2483
  try:
2470
2484
  self.save_nodes(directory)
@@ -2725,7 +2739,7 @@ class Network_3D:
2725
2739
  items = directory_info(directory)
2726
2740
 
2727
2741
  for item in items:
2728
- if item == 'node_communities.xlsx':
2742
+ if item == 'node_communities.xlsx' or item == 'node_communities.csv':
2729
2743
  if directory is not None:
2730
2744
  self._communities = network_analysis.read_excel_to_singval_dict(f'{directory}/{item}')
2731
2745
  print("Succesfully loaded communities")
@@ -2777,7 +2791,7 @@ class Network_3D:
2777
2791
  items = directory_info(directory)
2778
2792
 
2779
2793
  for item in items:
2780
- if item == 'drawn_network.tif':
2794
+ if item == 'overlay_1.tif':
2781
2795
  if directory is not None:
2782
2796
  self._network_overlay = tifffile.imread(f'{directory}/{item}')
2783
2797
  print("Succesfully loaded network overlay")
@@ -2802,7 +2816,7 @@ class Network_3D:
2802
2816
  items = directory_info(directory)
2803
2817
 
2804
2818
  for item in items:
2805
- if item == 'labelled_node_indices.tif':
2819
+ if item == 'overlay_2.tif':
2806
2820
  if directory is not None:
2807
2821
  self._id_overlay = tifffile.imread(f'{directory}/{item}')
2808
2822
  print("Succesfully loaded id overlay")
@@ -2816,7 +2830,7 @@ class Network_3D:
2816
2830
  #print("Could not find id overlay. They must be in the specified directory and named 'labelled_node_indices.tif'")
2817
2831
 
2818
2832
 
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):
2833
+ 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
2834
  """
2821
2835
  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
2836
  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 +2854,7 @@ class Network_3D:
2840
2854
  self.load_node_identities(directory, node_identities_path)
2841
2855
  self.load_edge_centroids(directory, edge_centroids_path)
2842
2856
  self.load_scaling(directory, scaling_path)
2857
+ self.load_communities(directory, community_path)
2843
2858
  self.load_network_overlay(directory, net_overlay_path)
2844
2859
  self.load_id_overlay(directory, id_overlay_path)
2845
2860
 
@@ -3132,8 +3147,7 @@ class Network_3D:
3132
3147
  """
3133
3148
 
3134
3149
 
3135
- if directory is None:
3136
- directory = encapsulate()
3150
+ directory = encapsulate()
3137
3151
 
3138
3152
  self._xy_scale = xy_scale
3139
3153
  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,14 @@ 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
161
160
 
162
161
  return master_list
163
162
 
@@ -576,34 +575,35 @@ def find_centroids(nodes, down_factor = None, network = None):
576
575
 
577
576
  return centroid_dict
578
577
 
579
- def _save_centroid_dictionary(centroid_dict, filepath = None, index = 'Node ID'):
578
+ def _save_centroid_dictionary(centroid_dict, filepath=None, index='Node ID'):
580
579
  # 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
580
  df = pd.DataFrame.from_dict(centroid_dict, orient='index', columns=['Z', 'Y', 'X'])
591
-
592
- # Rename the index to 'Node ID'
581
+
582
+ # Rename the index to specified name
593
583
  df.index.name = index
594
-
584
+
595
585
  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")
586
+ base_path = 'centroids'
601
587
  else:
588
+ # Remove file extension if present to use as base path
589
+ base_path = filepath.rsplit('.', 1)[0]
590
+
591
+ # First try to save as CSV
592
+ try:
593
+ csv_path = f"{base_path}.csv"
594
+ df.to_csv(csv_path)
595
+ print(f"Successfully saved centroids to {csv_path}")
596
+ return
597
+ except Exception as e:
598
+ print(f"Could not save centroids as CSV: {str(e)}")
599
+
600
+ # If CSV fails, try to save as Excel
602
601
  try:
603
- # Save DataFrame to Excel file
604
- df.to_excel(filepath, engine='openpyxl')
602
+ xlsx_path = f"{base_path}.xlsx"
603
+ df.to_excel(xlsx_path, engine='openpyxl')
604
+ print(f"Successfully saved centroids to {xlsx_path}")
605
605
  except Exception as e:
606
- print(f"Could not save centroids to {filepath}")
606
+ print(f"Could not save centroids as XLSX: {str(e)}")
607
607
 
608
608
  def _find_centroids_GPU(nodes, node_list=None, down_factor=None):
609
609
  """Internal use version to get centroids without saving"""
@@ -1229,15 +1229,31 @@ def edge_to_node(network, node_identities = None):
1229
1229
 
1230
1230
 
1231
1231
  def save_singval_dict(dict, index_name, valname, filename):
1232
-
1233
- #index name goes on the left, valname on the right
1232
+ # Convert dictionary to DataFrame
1234
1233
  df = pd.DataFrame.from_dict(dict, orient='index', columns=[valname])
1235
-
1236
- # Rename the index to 'Node ID'
1234
+
1235
+ # Rename the index
1237
1236
  df.index.name = index_name
1238
-
1239
- # Save DataFrame to Excel file
1240
- df.to_excel(filename, engine='openpyxl')
1237
+
1238
+ # Remove file extension if present to use as base path
1239
+ base_path = filename.rsplit('.', 1)[0]
1240
+
1241
+ # First try to save as CSV
1242
+ try:
1243
+ csv_path = f"{base_path}.csv"
1244
+ df.to_csv(csv_path)
1245
+ print(f"Successfully saved {valname} data to {csv_path}")
1246
+ return
1247
+ except Exception as e:
1248
+ print(f"Could not save as CSV: {str(e)}")
1249
+
1250
+ # If CSV fails, try to save as Excel
1251
+ try:
1252
+ xlsx_path = f"{base_path}.xlsx"
1253
+ df.to_excel(xlsx_path, engine='openpyxl')
1254
+ print(f"Successfully saved {valname} data to {xlsx_path}")
1255
+ except Exception as e:
1256
+ print(f"Could not save as XLSX: {str(e)}")
1241
1257
 
1242
1258
 
1243
1259
  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.3.0
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