nettracer3d 0.4.1__tar.gz → 0.4.3__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 (26) hide show
  1. {nettracer3d-0.4.1/src/nettracer3d.egg-info → nettracer3d-0.4.3}/PKG-INFO +3 -9
  2. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/README.md +1 -8
  3. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/pyproject.toml +5 -1
  4. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/morphology.py +2 -0
  5. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/nettracer.py +133 -0
  6. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/nettracer_gui.py +472 -97
  7. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/proximity.py +3 -1
  8. nettracer3d-0.4.3/src/nettracer3d/run.py +9 -0
  9. {nettracer3d-0.4.1 → nettracer3d-0.4.3/src/nettracer3d.egg-info}/PKG-INFO +3 -9
  10. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d.egg-info/SOURCES.txt +1 -0
  11. nettracer3d-0.4.3/src/nettracer3d.egg-info/entry_points.txt +2 -0
  12. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d.egg-info/requires.txt +1 -0
  13. nettracer3d-0.4.1/src/nettracer3d/run.py +0 -5
  14. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/LICENSE +0 -0
  15. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/setup.cfg +0 -0
  16. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/__init__.py +0 -0
  17. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/community_extractor.py +0 -0
  18. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/hub_getter.py +0 -0
  19. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/modularity.py +0 -0
  20. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/network_analysis.py +0 -0
  21. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/network_draw.py +0 -0
  22. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/node_draw.py +0 -0
  23. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/simple_network.py +0 -0
  24. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d/smart_dilate.py +0 -0
  25. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  26. {nettracer3d-0.4.1 → nettracer3d-0.4.3}/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.4.1
3
+ Version: 0.4.3
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
@@ -23,6 +23,7 @@ Requires-Dist: pandas
23
23
  Requires-Dist: napari
24
24
  Requires-Dist: python-louvain
25
25
  Requires-Dist: tifffile
26
+ Requires-Dist: qtrangeslider
26
27
  Requires-Dist: PyQt6
27
28
  Provides-Extra: cuda11
28
29
  Requires-Dist: cupy-cuda11x; extra == "cuda11"
@@ -31,15 +32,8 @@ Requires-Dist: cupy-cuda12x; extra == "cuda12"
31
32
  Provides-Extra: cupy
32
33
  Requires-Dist: cupy; extra == "cupy"
33
34
 
34
- NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity or volumetric thresholding. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, run a python script in your env with the following commands:
35
+ NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity or volumetric thresholding. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, enter the command 'nettracer3d' in your command prompt:
35
36
 
36
- #Start
37
-
38
- from nettracer3d import nettracer_gui
39
-
40
- nettracer_gui.run_gui()
41
-
42
- #End
43
37
 
44
38
  This gui is built from the PyQt6 package and therefore may not function on dockers or virtual envs that are unable to support PyQt6 displays. More advanced documentation (especially for the GUI) is coming down the line, but for now please see: https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link
45
39
  for a user manual that provides older documentation.
@@ -1,12 +1,5 @@
1
- NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity or volumetric thresholding. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, run a python script in your env with the following commands:
1
+ NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity or volumetric thresholding. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, enter the command 'nettracer3d' in your command prompt:
2
2
 
3
- #Start
4
-
5
- from nettracer3d import nettracer_gui
6
-
7
- nettracer_gui.run_gui()
8
-
9
- #End
10
3
 
11
4
  This gui is built from the PyQt6 package and therefore may not function on dockers or virtual envs that are unable to support PyQt6 displays. More advanced documentation (especially for the GUI) is coming down the line, but for now please see: https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link
12
5
  for a user manual that provides older documentation.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.4.1"
3
+ version = "0.4.3"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="boom2449@gmail.com" },
6
6
  ]
@@ -18,6 +18,7 @@ dependencies = [
18
18
  "napari",
19
19
  "python-louvain",
20
20
  "tifffile",
21
+ "qtrangeslider",
21
22
  "PyQt6"
22
23
  ]
23
24
  readme = "README.md"
@@ -33,6 +34,9 @@ CUDA11 = ["cupy-cuda11x"]
33
34
  CUDA12 = ["cupy-cuda12x"]
34
35
  cupy = ["cupy"]
35
36
 
37
+ [project.scripts]
38
+ nettracer3d = "nettracer3d.run:main"
39
+
36
40
  [project.urls]
37
41
  User_Manual = "https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link"
38
42
  Reference_Citation_For_Use = "https://doi.org/10.1101/2024.07.29.605633"
@@ -101,6 +101,8 @@ def process_label(args):
101
101
  nodes, edges, label, dilate_xy, dilate_z, array_shape = args
102
102
  print(f"Processing node {label}")
103
103
  indices = np.argwhere(nodes == label)
104
+ if len(indices) == 0:
105
+ return None, None, None
104
106
  z_vals, y_vals, x_vals = get_reslice_indices((indices, dilate_xy, dilate_z, array_shape))
105
107
  if z_vals is None: #If get_reslice_indices ran into a ValueError, nothing is returned.
106
108
  return None, None, None
@@ -1003,6 +1003,8 @@ def dilate_3D_recursive(tiff_array, dilated_x, dilated_y, dilated_z, step_size=N
1003
1003
  max_dilation = max(dilated_x, dilated_y, dilated_z)
1004
1004
  if max_dilation < (0.2 * min_dim):
1005
1005
  return dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z)
1006
+ elif dilated_x == 1 and dilated_y == 1 and dilated_z == 1: #Also if there is only a single dilation don't do it
1007
+ return dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z)
1006
1008
 
1007
1009
  # Initialize step_size for first call
1008
1010
  if step_size is None:
@@ -1587,6 +1589,51 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
1587
1589
 
1588
1590
  return array
1589
1591
 
1592
+ def fix_branches(array, G, communities, fix_val = None):
1593
+
1594
+ def invert_dict(d):
1595
+ inverted = {}
1596
+ for key, value in d.items():
1597
+ inverted.setdefault(value, []).append(key)
1598
+ return inverted
1599
+
1600
+ def get_degree_threshold(community_degrees):
1601
+ degrees = np.array(community_degrees, dtype=float)
1602
+ hist, bins = np.histogram(degrees, bins='auto')
1603
+ peaks, _ = find_peaks(hist)
1604
+ if len(peaks) > 1:
1605
+ # Get bin value after first peak as threshold
1606
+ return bins[peaks[0] + 1]
1607
+ return 4 # Default fallback
1608
+
1609
+ avg_degree = G.number_of_edges() * 2 / G.number_of_nodes()
1610
+
1611
+ targs = []
1612
+
1613
+ inverted = invert_dict(communities)
1614
+
1615
+ community_degrees = {}
1616
+
1617
+ for com in inverted:
1618
+ subgraph = G.subgraph(inverted[com])
1619
+ sub_degree = subgraph.number_of_edges() * 2/ subgraph.number_of_nodes()
1620
+ community_degrees[com] = sub_degree
1621
+
1622
+
1623
+ if fix_val is None:
1624
+ threshold = get_degree_threshold(list(community_degrees.values()))
1625
+ else:
1626
+ threshold = fix_val
1627
+
1628
+ for com in community_degrees:
1629
+ if community_degrees[com] > threshold: #This method of comparison could possibly be more nuanced.
1630
+ targs.append(com)
1631
+
1632
+
1633
+ return targs
1634
+
1635
+
1636
+
1590
1637
  def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = 0, directory = None, return_skele = False, order = 0):
1591
1638
  """
1592
1639
  Can be used to label vertices (where multiple branches connect) a binary image. Labelled output will be saved to the active directory if none is specified. Note this works better on already thin filaments and may over-divide larger trunkish objects.
@@ -3417,6 +3464,92 @@ class Network_3D:
3417
3464
  self._nodes = self._nodes.astype(np.uint16)
3418
3465
 
3419
3466
 
3467
+ def com_to_node(self, targets = None):
3468
+
3469
+ def invert_dict(d):
3470
+ inverted = {}
3471
+ for key, value in d.items():
3472
+ inverted.setdefault(value, []).append(key)
3473
+ return inverted
3474
+
3475
+ def update_array(array_3d, value_dict, targets = None):
3476
+ ref_array = copy.deepcopy(array_3d)
3477
+ if targets is None:
3478
+ for key, value_list in value_dict.items():
3479
+ for value in value_list:
3480
+ array_3d[ref_array == value] = key
3481
+ else:
3482
+ max_val = np.max(array_3d) + 1
3483
+ for key, value_list in value_dict.items():
3484
+ for value in value_list:
3485
+ array_3d[ref_array == value] = max_val
3486
+ max_val += 1
3487
+
3488
+ return array_3d
3489
+
3490
+ if 0 in self.communities.values():
3491
+ self.communities = {k: v + 1 for k, v in self.communities.items()}
3492
+ if targets is not None:
3493
+ for item in targets:
3494
+ item = item + 1
3495
+
3496
+ inverted = invert_dict(self.communities)
3497
+
3498
+
3499
+ if targets is not None:
3500
+ new_inverted = copy.deepcopy(inverted)
3501
+ for com in inverted:
3502
+ if com not in targets:
3503
+ del new_inverted[com]
3504
+ inverted = new_inverted
3505
+
3506
+
3507
+ if self._node_identities is not None:
3508
+ new_identities = {}
3509
+ for com in inverted:
3510
+ new_identities[com] = ""
3511
+
3512
+ list1 = self._network_lists[0] #Get network lists to change
3513
+ list2 = self._network_lists[1]
3514
+ list3 = self._network_lists[2]
3515
+
3516
+ for i in range(len(list1)):
3517
+ list1[i] = self.communities[list1[i]] #Set node at network list spot to its community instead
3518
+ list2[i] = self.communities[list2[i]]
3519
+ if list1[i] == list2[i]: #If the edge corresponding there joins different communities, it will not be set to 0
3520
+ list3[i] = 0
3521
+
3522
+
3523
+ self.network_lists = [list1, list2, list3]
3524
+
3525
+ if self._nodes is not None:
3526
+ self._nodes = update_array(self._nodes, inverted, targets = targets) #Set the array to match the new network
3527
+
3528
+ try:
3529
+
3530
+ if self._node_identities is not None:
3531
+
3532
+ for key, value_list in inverted.items():
3533
+ temp_dict = {}
3534
+ for value in value_list:
3535
+ if self._node_identities[value] in temp_dict:
3536
+ temp_dict[self._node_identities[value]] += 1
3537
+ else:
3538
+ temp_dict[self._node_identities[value]] = 1
3539
+ for id_type, num in temp_dict.items():
3540
+ new_identities[key] += f'ID {id_type}:{num}, '
3541
+
3542
+ self.node_identities = new_identities
3543
+ except:
3544
+ pass
3545
+
3546
+
3547
+
3548
+
3549
+
3550
+
3551
+
3552
+
3420
3553
  def trunk_to_node(self):
3421
3554
  """
3422
3555
  Converts the edge 'trunk' into a node. In this case, the trunk is the edge that creates the most node-node connections. There may be times when many nodes are connected by a single, expansive edge that obfuscates the rest of the edges. Converting the trunk to a node can better reveal these edges.
@@ -16,6 +16,7 @@ from nettracer3d import nettracer as n3d
16
16
  from nettracer3d import smart_dilate as sdl
17
17
  from nettracer3d import proximity as pxt
18
18
  from matplotlib.colors import LinearSegmentedColormap
19
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
19
20
  import pandas as pd
20
21
  from PyQt6.QtGui import (QFont, QCursor, QColor)
21
22
  import tifffile
@@ -176,6 +177,9 @@ class ImageViewerWindow(QMainWindow):
176
177
  buttons_layout.addWidget(self.pan_button)
177
178
 
178
179
  control_layout.addWidget(buttons_widget)
180
+
181
+ self.preview = False #Whether in preview mode or not
182
+ self.targs = None #Targets for preview mode
179
183
 
180
184
  # Create channel buttons
181
185
  self.channel_buttons = []
@@ -396,7 +400,7 @@ class ImageViewerWindow(QMainWindow):
396
400
  self.slice_slider.setValue(new_value)
397
401
 
398
402
 
399
- def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None):
403
+ def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
400
404
  """
401
405
  Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
402
406
 
@@ -410,6 +414,12 @@ class ImageViewerWindow(QMainWindow):
410
414
  mask = np.isin(chunk_data, indices_to_check)
411
415
  return mask * 255
412
416
 
417
+ def process_chunk_bounds(chunk_data, indices_to_check):
418
+ """Process a single chunk of the array to create highlight mask"""
419
+
420
+ mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
421
+ return mask * 255
422
+
413
423
  if node_indices is not None:
414
424
  if 0 in node_indices:
415
425
  node_indices.remove(0)
@@ -453,7 +463,7 @@ class ImageViewerWindow(QMainWindow):
453
463
  num_cores = mp.cpu_count()
454
464
 
455
465
  # Calculate chunk size along y-axis
456
- chunk_size = full_shape[0] // num_cores
466
+ chunk_size = full_shape[1] // num_cores
457
467
  if chunk_size < 1:
458
468
  chunk_size = 1
459
469
 
@@ -463,25 +473,32 @@ class ImageViewerWindow(QMainWindow):
463
473
 
464
474
  # Create chunks
465
475
  chunks = []
466
- for i in range(0, array_shape[0], chunk_size):
467
- end = min(i + chunk_size, array_shape[0])
468
- chunks.append(channel_data[i:end])
476
+ for i in range(0, array_shape[1], chunk_size):
477
+ end = min(i + chunk_size, array_shape[1])
478
+ chunks.append(channel_data[:, i:end, :])
469
479
 
470
480
  # Process chunks in parallel using ThreadPoolExecutor
471
- process_func = partial(process_chunk, indices_to_check=indices)
481
+ if not bounds:
482
+ process_func = partial(process_chunk, indices_to_check=indices)
483
+ else:
484
+ if len(indices) == 1:
485
+ indices.insert(0, 0)
486
+ process_func = partial(process_chunk_bounds, indices_to_check=indices)
487
+
472
488
 
473
489
  with ThreadPoolExecutor(max_workers=num_cores) as executor:
474
490
  chunk_results = list(executor.map(process_func, chunks))
475
491
 
476
492
  # Reassemble the chunks
477
- return np.vstack(chunk_results)
493
+ return np.concatenate(chunk_results, axis=1)
478
494
 
479
495
  # Process nodes and edges in parallel using multiprocessing
480
- with ThreadPoolExecutor(max_workers=2) as executor:
496
+ with ThreadPoolExecutor(max_workers=num_cores) as executor:
481
497
  future_nodes = executor.submit(process_channel, self.channel_data[0], node_indices, full_shape)
482
498
  future_edges = executor.submit(process_channel, self.channel_data[1], edge_indices, full_shape)
483
499
  future_overlay1 = executor.submit(process_channel, self.channel_data[2], overlay1_indices, full_shape)
484
500
  future_overlay2 = executor.submit(process_channel, self.channel_data[3], overlay2_indices, full_shape)
501
+
485
502
 
486
503
  # Get results
487
504
  node_overlay = future_nodes.result()
@@ -502,6 +519,84 @@ class ImageViewerWindow(QMainWindow):
502
519
  # Update display
503
520
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
504
521
 
522
+ def create_highlight_overlay_slice(self, indices, bounds = False):
523
+
524
+
525
+ def process_chunk_bounds(chunk_data, indices_to_check):
526
+ """Process a single chunk of the array to create highlight mask"""
527
+ mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
528
+ return mask * 255
529
+
530
+ def process_chunk(chunk_data, indices_to_check):
531
+ """Process a single chunk of the array to create highlight mask"""
532
+
533
+ mask = np.isin(chunk_data, indices_to_check)
534
+ return mask * 255
535
+
536
+ array = self.channel_data[self.active_channel]
537
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
538
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
539
+
540
+ current_slice = array[self.current_slice, :, :]
541
+ full_shape = array.shape
542
+ slice_shape = current_slice.shape
543
+
544
+ if self.highlight_overlay is None:
545
+
546
+ self.highlight_overlay = np.zeros(full_shape, dtype=np.uint8)
547
+
548
+ # Get number of CPU cores
549
+ num_cores = mp.cpu_count()
550
+
551
+ # Calculate chunk size along y-axis
552
+ chunk_size = slice_shape[0] // num_cores
553
+ if chunk_size < 1:
554
+ chunk_size = 1
555
+
556
+ def process_channel(channel_data, indices, array_shape):
557
+ if channel_data is None or not indices:
558
+ return None
559
+
560
+ # Create chunks
561
+ chunks = []
562
+ for i in range(0, array_shape[0], chunk_size):
563
+ end = min(i + chunk_size, array_shape[0])
564
+ chunks.append(channel_data[i:end])
565
+
566
+ # Process chunks in parallel using ThreadPoolExecutor
567
+ if not bounds:
568
+ process_func = partial(process_chunk, indices_to_check=indices)
569
+ else:
570
+ if len(indices) == 1:
571
+ indices.insert(0, 0)
572
+ process_func = partial(process_chunk_bounds, indices_to_check=indices)
573
+
574
+
575
+ with ThreadPoolExecutor(max_workers=num_cores) as executor:
576
+ chunk_results = list(executor.map(process_func, chunks))
577
+
578
+ # Reassemble the chunks
579
+ return np.vstack(chunk_results)
580
+
581
+ # Process nodes and edges in parallel using multiprocessing
582
+ with ThreadPoolExecutor(max_workers=num_cores) as executor:
583
+ future_highlight = executor.submit(process_channel, current_slice, indices, slice_shape)
584
+
585
+ # Get results
586
+ overlay = future_highlight.result()
587
+
588
+ try:
589
+
590
+ self.highlight_overlay[self.current_slice, :, :] = overlay
591
+ except:
592
+ pass
593
+
594
+ # Update display
595
+ self.update_display(preserve_zoom=(current_xlim, current_ylim), called = True)
596
+
597
+
598
+
599
+
505
600
 
506
601
 
507
602
 
@@ -1529,6 +1624,7 @@ class ImageViewerWindow(QMainWindow):
1529
1624
  self.selection_rect = None
1530
1625
  self.canvas.draw()
1531
1626
 
1627
+
1532
1628
  def highlight_value_in_tables(self, clicked_value):
1533
1629
  """Helper method to find and highlight a value in both tables."""
1534
1630
 
@@ -2055,8 +2151,9 @@ class ImageViewerWindow(QMainWindow):
2055
2151
 
2056
2152
  def show_thresh_dialog(self):
2057
2153
  """Show threshold dialog"""
2058
- thresh_window = ThresholdWindow(self)
2059
- thresh_window.show() # Non-modal window
2154
+ dialog = ThresholdDialog(self)
2155
+ dialog.exec()
2156
+
2060
2157
 
2061
2158
  def show_mask_dialog(self):
2062
2159
  """Show the mask dialog"""
@@ -2265,13 +2362,10 @@ class ImageViewerWindow(QMainWindow):
2265
2362
 
2266
2363
 
2267
2364
  except Exception as e:
2268
- import traceback
2269
- print(traceback.format_exc())
2270
2365
  print(f"An error has occured: {e}")
2271
2366
 
2272
2367
  except Exception as e:
2273
- import traceback
2274
- print(traceback.format_exc())
2368
+
2275
2369
  QMessageBox.critical(
2276
2370
  self,
2277
2371
  "Error Loading",
@@ -2513,12 +2607,22 @@ class ImageViewerWindow(QMainWindow):
2513
2607
  """Shows a dialog asking user to confirm if image is 2D RGB"""
2514
2608
  msg = QMessageBox()
2515
2609
  msg.setIcon(QMessageBox.Icon.Question)
2516
- msg.setText("Image Format Detection")
2610
+ msg.setText("Image Format Alert")
2517
2611
  msg.setInformativeText("Is this a 2D color (RGB/CMYK) image?")
2518
2612
  msg.setWindowTitle("Confirm Image Format")
2519
2613
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
2520
2614
  return msg.exec() == QMessageBox.StandardButton.Yes
2521
2615
 
2616
+ def confirm_resize_dialog(self):
2617
+ """Shows a dialog asking user to resize image"""
2618
+ msg = QMessageBox()
2619
+ msg.setIcon(QMessageBox.Icon.Question)
2620
+ msg.setText("Image Format Alert")
2621
+ msg.setInformativeText(f"This image is a different shape than the ones loaded into the viewer window. Trying to run processes with images of different sizes has a high probability of crashing the program.\nPress yes to resize the new image to the other images. Press no to load it anyway.")
2622
+ msg.setWindowTitle("Resize")
2623
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
2624
+ return msg.exec() == QMessageBox.StandardButton.Yes
2625
+
2522
2626
  def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
2523
2627
  """Load a channel and enable active channel selection if needed."""
2524
2628
 
@@ -2547,6 +2651,19 @@ class ImageViewerWindow(QMainWindow):
2547
2651
  if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
2548
2652
  self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index])
2549
2653
 
2654
+ for i in range(4): #Try to ensure users don't load in different sized arrays
2655
+ if self.channel_data[i] is None or i == channel_index or data:
2656
+ if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
2657
+ if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
2658
+ self.highlight_overlay = None
2659
+ continue
2660
+ else:
2661
+ old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
2662
+ if old_shape != self.channel_data[channel_index].shape[:3]:
2663
+ if self.confirm_resize_dialog():
2664
+ self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
2665
+ break
2666
+
2550
2667
 
2551
2668
  if channel_index == 0:
2552
2669
  my_network.nodes = self.channel_data[channel_index]
@@ -2601,7 +2718,10 @@ class ImageViewerWindow(QMainWindow):
2601
2718
  self.volume_dict[channel_index] = None #reset volumes
2602
2719
 
2603
2720
  if assign_shape: #keep original shape tracked to undo resampling.
2604
- self.original_shape = self.channel_data[channel_index].shape
2721
+ if self.original_shape is None:
2722
+ self.original_shape = self.channel_data[channel_index].shape
2723
+ elif self.original_shape[0] < self.channel_data[channel_index].shape[0] or self.original_shape[1] < self.channel_data[channel_index].shape[1] or self.original_shape[2] < self.channel_data[channel_index].shape[2]:
2724
+ self.original_shape = self.channel_data[channel_index].shape
2605
2725
  if len(self.original_shape) == 4:
2606
2726
  self.original_shape = (self.original_shape[0], self.original_shape[1], self.original_shape[2])
2607
2727
 
@@ -2829,11 +2949,11 @@ class ImageViewerWindow(QMainWindow):
2829
2949
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2830
2950
  # Convert slider values (0-100) to data values (0-1)
2831
2951
  min_val, max_val = values
2832
- self.channel_brightness[channel_index]['min'] = min_val / 255
2952
+ self.channel_brightness[channel_index]['min'] = min_val / 255 #Accomodate 32 bit data?
2833
2953
  self.channel_brightness[channel_index]['max'] = max_val / 255
2834
2954
  self.update_display(preserve_zoom = (current_xlim, current_ylim))
2835
2955
 
2836
- def update_display(self, preserve_zoom=None, dims = None):
2956
+ def update_display(self, preserve_zoom=None, dims = None, called = False):
2837
2957
  """Update the display with currently visible channels and highlight overlay."""
2838
2958
 
2839
2959
  self.figure.clear()
@@ -2927,6 +3047,9 @@ class ImageViewerWindow(QMainWindow):
2927
3047
  vmax=1,
2928
3048
  extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
2929
3049
 
3050
+ if self.preview and not called:
3051
+ self.create_highlight_overlay_slice(self.targs, bounds = self.bounds)
3052
+
2930
3053
  # Add highlight overlay if it exists
2931
3054
  if self.highlight_overlay is not None:
2932
3055
  highlight_slice = self.highlight_overlay[self.current_slice]
@@ -2939,6 +3062,7 @@ class ImageViewerWindow(QMainWindow):
2939
3062
  alpha=0.5)
2940
3063
 
2941
3064
 
3065
+
2942
3066
 
2943
3067
  # Style the axes
2944
3068
  self.ax.set_xlabel('X')
@@ -4162,17 +4286,18 @@ class WhiteDialog(QDialog):
4162
4286
  def white_overlay(self):
4163
4287
 
4164
4288
  try:
4165
-
4166
- try:
4289
+ if isinstance(my_network.nodes, np.ndarray) :
4167
4290
  overlay = np.ones_like(my_network.nodes).astype(np.uint8) * 255
4168
- except:
4291
+ elif isinstance(my_network.edges, np.ndarray):
4169
4292
  overlay = np.ones_like(my_network.edges).astype(np.uint8) * 255
4170
- finally:
4171
- my_network.id_overlay = overlay
4293
+ elif isinstance(my_network.network_overlay, np.ndarray):
4294
+ overlay = np.ones_like(my_network.network_overlay).astype(np.uint8) * 255
4172
4295
 
4173
- self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
4296
+ my_network.id_overlay = overlay
4174
4297
 
4175
- self.accept()
4298
+ self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
4299
+
4300
+ self.accept()
4176
4301
 
4177
4302
  except Exception as e:
4178
4303
  print(f"Error making white background: {e}")
@@ -4274,7 +4399,7 @@ class NetShowDialog(QDialog):
4274
4399
  self.geo_layout = QPushButton("geo_layout")
4275
4400
  self.geo_layout.setCheckable(True)
4276
4401
  self.geo_layout.setChecked(False)
4277
- layout.addRow("Use Geometric Layout:", self.geo_layout)
4402
+ layout.addRow("Use Geographic Layout:", self.geo_layout)
4278
4403
 
4279
4404
  # Add mode selection dropdown
4280
4405
  self.mode_selector = QComboBox()
@@ -5246,101 +5371,303 @@ class LabelDialog(QDialog):
5246
5371
  f"Error running label: {str(e)}"
5247
5372
  )
5248
5373
 
5249
- class ThresholdWindow(QMainWindow):
5374
+ class ThresholdDialog(QDialog):
5250
5375
  def __init__(self, parent=None):
5251
5376
  super().__init__(parent)
5252
- self.setWindowTitle("Threshold Params (Active Image)")
5253
-
5254
- # Create central widget and layout
5255
- central_widget = QWidget()
5256
- self.setCentralWidget(central_widget)
5257
- layout = QFormLayout(central_widget)
5258
-
5259
- self.min = QLineEdit("")
5260
- layout.addRow("Minimum Value to retain:", self.min)
5377
+ self.setWindowTitle("Choose Threshold Mode")
5378
+ self.setModal(True)
5261
5379
 
5262
- # Create widgets
5263
- self.max = QLineEdit("")
5264
- layout.addRow("Maximum Value to retain:", self.max)
5380
+ layout = QFormLayout(self)
5265
5381
 
5266
5382
  # Add mode selection dropdown
5267
5383
  self.mode_selector = QComboBox()
5268
- self.mode_selector.addItems(["Using Volumes", "Using Label/Brightness"])
5384
+ self.mode_selector.addItems(["Using Label/Brightness", "Using Volumes"])
5269
5385
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5270
5386
  layout.addRow("Execution Mode:", self.mode_selector)
5271
5387
 
5272
5388
  # Add Run button
5273
- prev_button = QPushButton("Preview")
5274
- prev_button.clicked.connect(self.run_preview)
5275
- layout.addRow(prev_button)
5389
+ run_button = QPushButton("Select")
5390
+ run_button.clicked.connect(self.thresh_mode)
5391
+ layout.addRow(run_button)
5392
+
5393
+ def thresh_mode(self):
5394
+
5395
+ try:
5396
+
5397
+ accepted_mode = self.mode_selector.currentIndex()
5398
+
5399
+ if accepted_mode == 1:
5400
+ if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
5401
+ self.parent().show_label_dialog()
5402
+
5403
+ if self.parent().volume_dict[self.parent().active_channel] is None:
5404
+ self.parent().volumes()
5405
+
5406
+ thresh_window = ThresholdWindow(self.parent(), accepted_mode)
5407
+ thresh_window.show() # Non-modal window
5408
+ self.highlight_overlay = None
5409
+ self.accept()
5410
+ except:
5411
+ pass
5412
+
5413
+
5414
+
5415
+
5416
+
5417
+ class ThresholdWindow(QMainWindow):
5418
+ def __init__(self, parent=None, accepted_mode=0):
5419
+ super().__init__(parent)
5420
+ self.setWindowTitle("Threshold")
5276
5421
 
5277
- # Add Run button
5422
+ # Create central widget and layout
5423
+ central_widget = QWidget()
5424
+ self.setCentralWidget(central_widget)
5425
+ layout = QVBoxLayout(central_widget)
5426
+
5427
+ # Get histogram data
5428
+ if accepted_mode == 1:
5429
+ self.histo_list = list(self.parent().volume_dict[self.parent().active_channel].values())
5430
+ self.bounds = False
5431
+ self.parent().bounds = False
5432
+ elif accepted_mode == 0:
5433
+ self.histo_list = self.parent().channel_data[self.parent().active_channel].flatten().tolist()
5434
+ self.bounds = True
5435
+ self.parent().bounds = True
5436
+
5437
+ # Create matplotlib figure
5438
+ fig = Figure(figsize=(5, 4))
5439
+ self.canvas = FigureCanvas(fig)
5440
+ layout.addWidget(self.canvas)
5441
+
5442
+ # Pre-compute histogram with numpy
5443
+ counts, bin_edges = np.histogram(self.histo_list, bins=50)
5444
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
5445
+
5446
+ # Plot pre-computed histogram
5447
+ self.ax = fig.add_subplot(111)
5448
+ self.ax.bar(bin_centers, counts, width=bin_edges[1] - bin_edges[0], alpha=0.5)
5449
+
5450
+ # Add vertical lines for thresholds
5451
+ self.min_line = self.ax.axvline(min(self.histo_list), color='r')
5452
+ self.max_line = self.ax.axvline(max(self.histo_list), color='b')
5453
+
5454
+ # Connect events for dragging
5455
+ self.canvas.mpl_connect('button_press_event', self.on_press)
5456
+ self.canvas.mpl_connect('motion_notify_event', self.on_motion)
5457
+ self.canvas.mpl_connect('button_release_event', self.on_release)
5458
+
5459
+ self.dragging = None
5460
+
5461
+ # Store histogram bounds
5462
+ if self.bounds:
5463
+ self.data_min = 0
5464
+ else:
5465
+ self.data_min = min(self.histo_list)
5466
+ self.data_max = max(self.histo_list)
5467
+
5468
+ # Create form layout for inputs
5469
+ form_layout = QFormLayout()
5470
+
5471
+ self.min = QLineEdit(f"{self.data_min}")
5472
+ self.min.editingFinished.connect(self.min_value_changed)
5473
+ form_layout.addRow("Minimum Value to retain:", self.min)
5474
+ self.prev_min = self.data_min
5475
+
5476
+ self.max = QLineEdit(f"{self.data_max}")
5477
+ self.max.editingFinished.connect(self.max_value_changed)
5478
+ form_layout.addRow("Maximum Value to retain:", self.max)
5479
+ self.prev_max = self.data_max
5480
+
5481
+ self.targs = [self.prev_min, self.prev_max]
5482
+
5483
+ # preview checkbox (default False)
5484
+ self.preview = QPushButton("Preview")
5485
+ self.preview.setCheckable(True)
5486
+ self.preview.setChecked(False)
5487
+ self.preview.clicked.connect(self.preview_mode)
5488
+ form_layout.addRow("Show Preview:", self.preview)
5489
+
5278
5490
  run_button = QPushButton("Apply Threshold")
5279
5491
  run_button.clicked.connect(self.thresh)
5280
- layout.addRow(run_button)
5492
+ form_layout.addRow(run_button)
5281
5493
 
5282
- # Set a reasonable default size
5283
- self.setMinimumWidth(300)
5494
+ layout.addLayout(form_layout)
5284
5495
 
5285
- def run_preview(self):
5496
+ # Set a reasonable default size
5497
+ self.setMinimumWidth(400)
5498
+ self.setMinimumHeight(400)
5286
5499
 
5287
- def get_valid_float(text, default_value):
5288
- try:
5289
- return float(text) if text.strip() else default_value
5290
- except ValueError:
5291
- print(f"Invalid input: {text}")
5292
- return default_value
5500
+ def closeEvent(self, event):
5501
+ self.parent().preview = False
5502
+ self.parent().targs = None
5503
+ self.parent().bounds = False
5293
5504
 
5505
+ def get_values_in_range(self, lst, min_val, max_val):
5506
+ values = [x for x in lst if min_val <= x <= max_val]
5507
+ output = []
5508
+ for item in self.parent().volume_dict[self.parent().active_channel]:
5509
+ if self.parent().volume_dict[self.parent().active_channel][item] in values:
5510
+ output.append(item)
5511
+ return output
5512
+
5513
+
5514
+ def min_value_changed(self):
5294
5515
  try:
5295
- channel = self.parent().active_channel
5296
- accepted_mode = self.mode_selector.currentIndex()
5516
+ text = self.min.text()
5517
+ if not text: # If empty, ignore
5518
+ return
5297
5519
 
5298
- if accepted_mode == 0:
5299
- if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
5300
- self.parent().show_label_dialog()
5520
+ try:
5521
+ value = float(text)
5522
+
5523
+ # Bound check against data limits
5524
+ value = max(self.data_min, value)
5525
+
5526
+ # Check against max line
5527
+ max_val = float(self.max.text()) if self.max.text() else self.data_max
5528
+ if value > max_val:
5529
+ # If min would exceed max, set max to its highest possible value
5530
+ self.max.setText(str(round(self.data_max, 2)))
5531
+ self.max_line.set_xdata([self.data_max, self.data_max])
5532
+ # And set min to the previous max value
5533
+ value = max_val
5534
+ self.min.setText(str(round(value, 2)))
5535
+
5536
+ if value == self.prev_min:
5537
+ return
5538
+ else:
5539
+ self.prev_min = value
5540
+ if self.bounds:
5541
+ self.targs = [self.prev_min, self.prev_max]
5542
+ else:
5543
+ self.targs = self.get_values_in_range(self.histo_list, self.prev_min, self.prev_max)
5544
+ self.parent().targs = self.targs
5545
+ if self.preview.isChecked():
5546
+ self.parent().highlight_overlay = None
5547
+ self.parent().create_highlight_overlay_slice(self.targs, bounds = self.bounds)
5548
+
5549
+ # Update the line
5550
+ self.min_line.set_xdata([value, value])
5551
+ self.canvas.draw()
5552
+
5301
5553
 
5302
- if self.parent().volume_dict[channel] is None:
5303
- self.parent().volumes()
5304
-
5305
- volumes = self.parent().volume_dict[channel]
5306
- default_max = max(volumes.values())
5307
- default_min = min(volumes.values())
5308
5554
 
5309
- max_val = get_valid_float(self.max.text(), default_max)
5310
- min_val = get_valid_float(self.min.text(), default_min)
5555
+ except ValueError:
5556
+ # If invalid number, reset to current line position
5557
+ self.min.setText(str(round(self.min_line.get_xself.data_mindata()[0], 2)))
5558
+ except:
5559
+ pass
5560
+
5561
+ def max_value_changed(self):
5562
+ try:
5563
+ text = self.max.text()
5564
+ if not text: # If empty, ignore
5565
+ return
5311
5566
 
5312
- valid_indices = [item for item in volumes
5313
- if min_val <= volumes[item] <= max_val]
5314
-
5315
- elif accepted_mode == 1:
5316
- channel_data = self.parent().channel_data[self.parent().active_channel]
5317
- default_max = np.max(channel_data)
5318
- default_min = np.min(channel_data)
5567
+ try:
5568
+ value = float(text)
5319
5569
 
5320
- max_val = int(get_valid_float(self.max.text(), default_max))
5321
- min_val = int(get_valid_float(self.min.text(), default_min))
5570
+ # Bound check against data limits
5571
+ value = min(self.data_max, value)
5322
5572
 
5323
- if min_val > max_val:
5324
- min_val, max_val = max_val, min_val
5325
-
5326
- valid_indices = list(range(min_val, max_val + 1))
5573
+ # Check against min line
5574
+ min_val = float(self.min.text()) if self.min.text() else self.data_min
5575
+ if value < min_val:
5576
+ # If max would go below min, set min to its lowest possible value
5577
+ self.min.setText(str(round(self.data_min, 2)))
5578
+ self.min_line.set_xdata([self.data_min, self.data_min])
5579
+ # And set max to the previous min value
5580
+ value = min_val
5581
+ self.max.setText(str(round(value, 2)))
5582
+
5583
+ if value == self.prev_max:
5584
+ return
5585
+ else:
5586
+ self.prev_max = value
5587
+ if self.bounds:
5588
+ self.targs = [self.prev_min, self.prev_max]
5589
+ else:
5590
+ self.targs = self.get_values_in_range(self.histo_list, self.prev_min, self.prev_max)
5591
+ self.parent().targs = self.targs
5592
+ if self.preview.isChecked():
5593
+ self.parent().highlight_overlay = None
5594
+ self.parent().create_highlight_overlay_slice(self.targs, bounds = self.bounds)
5595
+
5596
+ # Update the line
5597
+ self.max_line.set_xdata([value, value])
5598
+ self.canvas.draw()
5599
+
5600
+
5601
+
5602
+
5603
+
5604
+
5605
+ except ValueError:
5606
+ # If invalid number, reset to current line position
5607
+ self.max.setText(str(round(self.max_line.get_xdata()[0], 2)))
5608
+ except:
5609
+ pass
5610
+
5611
+ def on_press(self, event):
5612
+ try:
5613
+ if event.inaxes != self.ax:
5614
+ return
5327
5615
 
5328
- if channel == 0:
5329
- self.parent().create_highlight_overlay(node_indices = valid_indices)
5330
- elif channel == 1:
5331
- self.parent().create_highlight_overlay(edge_indices = valid_indices)
5332
- elif channel == 2:
5333
- self.parent().create_highlight_overlay(overlay1_indices = valid_indices)
5334
- elif channel == 3:
5335
- self.parent().create_highlight_overlay(overlay2_indices = valid_indices)
5616
+ # Left click controls left line
5617
+ if event.button == 1: # Left click
5618
+ self.dragging = 'min'
5619
+ # Right click controls right line
5620
+ elif event.button == 3: # Right click
5621
+ self.dragging = 'max'
5622
+ except:
5623
+ pass
5624
+
5625
+ def on_motion(self, event):
5626
+ try:
5627
+ if not self.dragging or event.inaxes != self.ax:
5628
+ return
5629
+
5630
+ if self.dragging == 'min':
5631
+ if event.xdata < self.max_line.get_xdata()[0]:
5632
+ self.min_line.set_xdata([event.xdata, event.xdata])
5633
+ self.min.setText(str(round(event.xdata, 2)))
5634
+ else:
5635
+ if event.xdata > self.min_line.get_xdata()[0]:
5636
+ self.max_line.set_xdata([event.xdata, event.xdata])
5637
+ self.max.setText(str(round(event.xdata, 2)))
5638
+
5639
+ self.canvas.draw()
5640
+ except:
5641
+ pass
5642
+
5643
+ def on_release(self, event):
5644
+ self.min_value_changed()
5645
+ self.max_value_changed()
5646
+ self.dragging = None
5336
5647
 
5337
- except Exception as e:
5338
- print(f"Error showing preview: {e}")
5648
+ def preview_mode(self):
5649
+ try:
5650
+ preview = self.preview.isChecked()
5651
+ self.parent().preview = preview
5652
+ self.parent().targs = self.targs
5653
+
5654
+ if preview and self.targs is not None:
5655
+ self.parent().create_highlight_overlay_slice(self.parent().targs, bounds = self.bounds)
5656
+ except:
5657
+ pass
5339
5658
 
5340
5659
  def thresh(self):
5341
5660
  try:
5342
5661
 
5343
- self.run_preview()
5662
+ if self.parent().active_channel == 0:
5663
+ self.parent().create_highlight_overlay(node_indices = self.targs, bounds = self.bounds)
5664
+ elif self.parent().active_channel == 1:
5665
+ self.parent().create_highlight_overlay(edge_indices = self.targs, bounds = self.bounds)
5666
+ elif self.parent().active_channel == 2:
5667
+ self.parent().create_highlight_overlay(overlay1_indices = self.targs, bounds = self.bounds)
5668
+ elif self.parent().active_channel == 3:
5669
+ self.parent().create_highlight_overlay(overlay2_indices = self.targs, bounds = self.bounds)
5670
+
5344
5671
  channel_data = self.parent().channel_data[self.parent().active_channel]
5345
5672
  mask = self.parent().highlight_overlay > 0
5346
5673
  channel_data = channel_data * mask
@@ -5349,6 +5676,8 @@ class ThresholdWindow(QMainWindow):
5349
5676
  self.close()
5350
5677
 
5351
5678
  except Exception as e:
5679
+ import traceback
5680
+ print(traceback.format_exc())
5352
5681
  QMessageBox.critical(
5353
5682
  self,
5354
5683
  "Error",
@@ -6119,11 +6448,21 @@ class BranchDialog(QDialog):
6119
6448
  self.nodes.setChecked(True)
6120
6449
  layout.addRow("Generate nodes from edges? (Skip if already completed - presumes your edge skeleton from generate nodes is in Edges and that your original Edges are in Overlay 2):", self.nodes)
6121
6450
 
6122
- # GPU checkbox (default True)
6451
+ # GPU checkbox (default False)
6123
6452
  self.GPU = QPushButton("GPU")
6124
6453
  self.GPU.setCheckable(True)
6125
6454
  self.GPU.setChecked(False)
6126
- layout.addRow("Use GPU (Note this may need to temporarily downsample your large images which may simplify outputs - Only memory errors but not permission errors for accessing GRAM are handled by default - CPU will never try to downsample):", self.GPU)
6455
+ layout.addRow("Use GPU (Note this may need to temporarily downsample your large images which may simplify outputs - Only memory errors but not permission errors for accessing VRAM are handled by default - CPU will never try to downsample):", self.GPU)
6456
+
6457
+ # Branch Fix checkbox (default False)
6458
+ self.fix = QPushButton("Auto-Correct Branches")
6459
+ self.fix.setCheckable(True)
6460
+ self.fix.setChecked(False)
6461
+ layout.addRow("Attempt to auto-correct branch labels:", self.fix)
6462
+
6463
+ self.fix_val = QLineEdit()
6464
+ self.fix_val.setPlaceholderText("Empty = default value...")
6465
+ layout.addRow("If checked above - Avg Degree of Nearby Branch Communities to Merge (Attempt to fix branch labeling - try 4 to 6 to start or leave empty):", self.fix_val)
6127
6466
 
6128
6467
  self.down_factor = QLineEdit("0")
6129
6468
  layout.addRow("Internal downsample (will have to recompute nodes)?:", self.down_factor)
@@ -6151,6 +6490,9 @@ class BranchDialog(QDialog):
6151
6490
  nodes = self.nodes.isChecked()
6152
6491
  GPU = self.GPU.isChecked()
6153
6492
  cubic = self.cubic.isChecked()
6493
+ fix = self.fix.isChecked()
6494
+ fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
6495
+
6154
6496
 
6155
6497
 
6156
6498
  original_shape = my_network.edges.shape
@@ -6158,7 +6500,7 @@ class BranchDialog(QDialog):
6158
6500
 
6159
6501
  if down_factor > 0:
6160
6502
  self.parent().show_gennodes_dialog(down_factor = [down_factor, cubic], called = True)
6161
- elif nodes:
6503
+ elif nodes or my_network.nodes is None:
6162
6504
  self.parent().show_gennodes_dialog(called = True)
6163
6505
  down_factor = None
6164
6506
 
@@ -6166,6 +6508,21 @@ class BranchDialog(QDialog):
6166
6508
 
6167
6509
  output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
6168
6510
 
6511
+ if fix:
6512
+
6513
+ temp_network = n3d.Network_3D(nodes = output)
6514
+
6515
+ temp_network.morph_proximity(search = 1) #Detect network of nearby branches
6516
+
6517
+ temp_network.community_partition(weighted = False, style = 1, dostats = False) #Find communities with louvain, unweighted params
6518
+
6519
+ targs = n3d.fix_branches(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
6520
+
6521
+ temp_network.com_to_node(targs)
6522
+
6523
+ output = temp_network.nodes
6524
+
6525
+
6169
6526
  if down_factor is not None:
6170
6527
 
6171
6528
  self.parent().reset(nodes = True, id_overlay = True, edges = True)
@@ -6181,6 +6538,8 @@ class BranchDialog(QDialog):
6181
6538
 
6182
6539
  except Exception as e:
6183
6540
  print(f"Error labeling branches: {e}")
6541
+ import traceback
6542
+ print(traceback.format_exc())
6184
6543
 
6185
6544
 
6186
6545
 
@@ -6336,7 +6695,7 @@ class AlterDialog(QDialog):
6336
6695
  class ModifyDialog(QDialog):
6337
6696
  def __init__(self, parent=None):
6338
6697
  super().__init__(parent)
6339
- self.setWindowTitle("Create Nodes from Edge Vertices")
6698
+ self.setWindowTitle("Modify Network Qualities")
6340
6699
  self.setModal(True)
6341
6700
  layout = QFormLayout(self)
6342
6701
 
@@ -6376,6 +6735,12 @@ class ModifyDialog(QDialog):
6376
6735
  self.isolate.setChecked(False)
6377
6736
  layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
6378
6737
 
6738
+ # Community collapse checkbox (default False)
6739
+ self.comcollapse = QPushButton("Communities -> nodes")
6740
+ self.comcollapse.setCheckable(True)
6741
+ self.comcollapse.setChecked(False)
6742
+ layout.addRow("Convert communities to nodes?:", self.comcollapse)
6743
+
6379
6744
  #change button
6380
6745
  change_button = QPushButton("Add/Remove Network Pairs")
6381
6746
  change_button.clicked.connect(self.show_alter_dialog)
@@ -6409,6 +6774,8 @@ class ModifyDialog(QDialog):
6409
6774
  edgeweight = self.edgeweight.isChecked()
6410
6775
  prune = self.prune.isChecked()
6411
6776
  isolate = self.isolate.isChecked()
6777
+ comcollapse = self.comcollapse.isChecked()
6778
+
6412
6779
 
6413
6780
  if isolate and my_network.node_identities is not None:
6414
6781
  self.show_isolate_dialog()
@@ -6434,6 +6801,15 @@ class ModifyDialog(QDialog):
6434
6801
  self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
6435
6802
  except:
6436
6803
  pass
6804
+ if comcollapse:
6805
+ if my_network.communities is None:
6806
+ self.parent().show_partition_dialog()
6807
+ if my_network.communities is None:
6808
+ return
6809
+ my_network.com_to_node()
6810
+ self.parent().load_channel(0, my_network.nodes, True)
6811
+ my_network.communities = None
6812
+
6437
6813
  try:
6438
6814
  if hasattr(my_network, 'network_lists'):
6439
6815
  model = PandasModel(my_network.network_lists)
@@ -6552,8 +6928,7 @@ class CentroidDialog(QDialog):
6552
6928
  "Error",
6553
6929
  f"Error finding centroids: {str(e)}"
6554
6930
  )
6555
- import traceback
6556
- print(traceback.format_exc())
6931
+
6557
6932
 
6558
6933
 
6559
6934
 
@@ -89,9 +89,11 @@ def process_label(args):
89
89
  nodes, label, dilate_xy, dilate_z, array_shape = args
90
90
  print(f"Processing node {label}")
91
91
  indices = np.argwhere(nodes == label)
92
+ if len(indices) == 0:
93
+ return None, None
92
94
  z_vals, y_vals, x_vals = get_reslice_indices((indices, dilate_xy, dilate_z, array_shape))
93
95
  if z_vals is None: #If get_reslice_indices ran into a ValueError, nothing is returned.
94
- return None, None, None
96
+ return None, None
95
97
  sub_nodes = reslice_3d_array((nodes, z_vals, y_vals, x_vals))
96
98
  return label, sub_nodes
97
99
 
@@ -0,0 +1,9 @@
1
+ from nettracer3d import nettracer_gui
2
+
3
+
4
+ def main():
5
+ nettracer_gui.run_gui()
6
+
7
+
8
+ if __name__ == '__main__':
9
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: nettracer3d
3
- Version: 0.4.1
3
+ Version: 0.4.3
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
@@ -23,6 +23,7 @@ Requires-Dist: pandas
23
23
  Requires-Dist: napari
24
24
  Requires-Dist: python-louvain
25
25
  Requires-Dist: tifffile
26
+ Requires-Dist: qtrangeslider
26
27
  Requires-Dist: PyQt6
27
28
  Provides-Extra: cuda11
28
29
  Requires-Dist: cupy-cuda11x; extra == "cuda11"
@@ -31,15 +32,8 @@ Requires-Dist: cupy-cuda12x; extra == "cuda12"
31
32
  Provides-Extra: cupy
32
33
  Requires-Dist: cupy; extra == "cupy"
33
34
 
34
- NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity or volumetric thresholding. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, run a python script in your env with the following commands:
35
+ NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity or volumetric thresholding. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, enter the command 'nettracer3d' in your command prompt:
35
36
 
36
- #Start
37
-
38
- from nettracer3d import nettracer_gui
39
-
40
- nettracer_gui.run_gui()
41
-
42
- #End
43
37
 
44
38
  This gui is built from the PyQt6 package and therefore may not function on dockers or virtual envs that are unable to support PyQt6 displays. More advanced documentation (especially for the GUI) is coming down the line, but for now please see: https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link
45
39
  for a user manual that provides older documentation.
@@ -18,5 +18,6 @@ src/nettracer3d/smart_dilate.py
18
18
  src/nettracer3d.egg-info/PKG-INFO
19
19
  src/nettracer3d.egg-info/SOURCES.txt
20
20
  src/nettracer3d.egg-info/dependency_links.txt
21
+ src/nettracer3d.egg-info/entry_points.txt
21
22
  src/nettracer3d.egg-info/requires.txt
22
23
  src/nettracer3d.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nettracer3d = nettracer3d.run:main
@@ -10,6 +10,7 @@ pandas
10
10
  napari
11
11
  python-louvain
12
12
  tifffile
13
+ qtrangeslider
13
14
  PyQt6
14
15
 
15
16
  [CUDA11]
@@ -1,5 +0,0 @@
1
- from . import nettracer_gui
2
-
3
-
4
- if __name__ == '__main__':
5
- nettracer_gui.run_gui()
File without changes
File without changes