nettracer3d 0.4.8__tar.gz → 0.5.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 (27) hide show
  1. {nettracer3d-0.4.8/src/nettracer3d.egg-info → nettracer3d-0.5.0}/PKG-INFO +15 -15
  2. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/pyproject.toml +15 -15
  3. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/nettracer_gui.py +220 -88
  4. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/segmenter.py +31 -28
  5. {nettracer3d-0.4.8 → nettracer3d-0.5.0/src/nettracer3d.egg-info}/PKG-INFO +15 -15
  6. nettracer3d-0.5.0/src/nettracer3d.egg-info/requires.txt +24 -0
  7. nettracer3d-0.4.8/src/nettracer3d.egg-info/requires.txt +0 -24
  8. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/LICENSE +0 -0
  9. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/README.md +0 -0
  10. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/setup.cfg +0 -0
  11. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/__init__.py +0 -0
  12. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/community_extractor.py +0 -0
  13. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/hub_getter.py +0 -0
  14. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/modularity.py +0 -0
  15. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/morphology.py +0 -0
  16. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/nettracer.py +0 -0
  17. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/network_analysis.py +0 -0
  18. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/network_draw.py +0 -0
  19. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/node_draw.py +0 -0
  20. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/proximity.py +0 -0
  21. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/run.py +0 -0
  22. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/simple_network.py +0 -0
  23. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/smart_dilate.py +0 -0
  24. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  25. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  26. {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  27. {nettracer3d-0.4.8 → nettracer3d-0.5.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.4.8
3
+ Version: 0.5.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
@@ -12,20 +12,20 @@ Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: numpy==1.26.4
15
- Requires-Dist: scipy
16
- Requires-Dist: scikit-image
17
- Requires-Dist: Pillow
18
- Requires-Dist: matplotlib
19
- Requires-Dist: networkx
20
- Requires-Dist: opencv-python-headless
21
- Requires-Dist: openpyxl
22
- Requires-Dist: pandas
23
- Requires-Dist: napari
24
- Requires-Dist: python-louvain
25
- Requires-Dist: tifffile
26
- Requires-Dist: qtrangeslider
27
- Requires-Dist: PyQt6
28
- Requires-Dist: scikit-learn
15
+ Requires-Dist: scipy==1.14.1
16
+ Requires-Dist: scikit-image==0.25.0
17
+ Requires-Dist: Pillow==11.1.0
18
+ Requires-Dist: matplotlib==3.9.2
19
+ Requires-Dist: networkx==3.2.1
20
+ Requires-Dist: opencv-python-headless==4.10.0.84
21
+ Requires-Dist: openpyxl==3.1.2
22
+ Requires-Dist: pandas==2.2.0
23
+ Requires-Dist: napari==0.5.5
24
+ Requires-Dist: python-louvain==0.16
25
+ Requires-Dist: tifffile==2023.7.18
26
+ Requires-Dist: qtrangeslider==0.1.5
27
+ Requires-Dist: PyQt6==6.8.0
28
+ Requires-Dist: scikit-learn==1.6.1
29
29
  Provides-Extra: cuda11
30
30
  Requires-Dist: cupy-cuda11x; extra == "cuda11"
31
31
  Provides-Extra: cuda12
@@ -1,26 +1,26 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.4.8"
3
+ version = "0.5.0"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="boom2449@gmail.com" },
6
6
  ]
7
7
  description = "Scripts for intializing and analyzing networks from segmentations of three dimensional images."
8
8
  dependencies = [
9
9
  "numpy == 1.26.4",
10
- "scipy",
11
- "scikit-image",
12
- "Pillow",
13
- "matplotlib",
14
- "networkx",
15
- "opencv-python-headless",
16
- "openpyxl",
17
- "pandas",
18
- "napari",
19
- "python-louvain",
20
- "tifffile",
21
- "qtrangeslider",
22
- "PyQt6",
23
- "scikit-learn"
10
+ "scipy == 1.14.1",
11
+ "scikit-image == 0.25.0",
12
+ "Pillow == 11.1.0",
13
+ "matplotlib == 3.9.2",
14
+ "networkx == 3.2.1",
15
+ "opencv-python-headless == 4.10.0.84",
16
+ "openpyxl == 3.1.2",
17
+ "pandas == 2.2.0",
18
+ "napari == 0.5.5",
19
+ "python-louvain == 0.16",
20
+ "tifffile == 2023.7.18",
21
+ "qtrangeslider == 0.1.5",
22
+ "PyQt6 == 6.8.0",
23
+ "scikit-learn == 1.6.1"
24
24
  ]
25
25
 
26
26
  readme = "README.md"
@@ -4,7 +4,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
4
4
  QHBoxLayout, QSlider, QMenuBar, QMenu, QDialog,
5
5
  QFormLayout, QLineEdit, QPushButton, QFileDialog,
6
6
  QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
7
- QMenu, QTabWidget)
7
+ QMenu, QTabWidget, QGroupBox)
8
8
  from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal)
9
9
  import numpy as np
10
10
  import time
@@ -196,6 +196,17 @@ class ImageViewerWindow(QMainWindow):
196
196
  buttons_layout.addWidget(self.high_button)
197
197
  self.highlight = True
198
198
 
199
+ self.pen_button = QPushButton("🖊️")
200
+ self.pen_button.setCheckable(True)
201
+ self.pen_button.setFixedSize(40, 40)
202
+ self.pen_button.clicked.connect(self.toggle_brush_mode)
203
+ buttons_layout.addWidget(self.pen_button)
204
+
205
+ self.thresh_button = QPushButton("✏️")
206
+ self.thresh_button.setFixedSize(40, 40)
207
+ self.thresh_button.clicked.connect(self.show_thresh_dialog)
208
+ buttons_layout.addWidget(self.thresh_button)
209
+
199
210
  control_layout.addWidget(buttons_widget)
200
211
 
201
212
  self.preview = False #Whether in preview mode or not
@@ -1289,11 +1300,12 @@ class ImageViewerWindow(QMainWindow):
1289
1300
  my_network.nodes = my_network.nodes * mask
1290
1301
  self.load_channel(0, my_network.nodes, True)
1291
1302
 
1292
- for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
1293
- if my_network.network_lists[0][i] in self.clicked_values['nodes'] or my_network.network_lists[0][i] in self.clicked_values['nodes']:
1294
- del my_network.network_lists[0][i]
1295
- del my_network.network_lists[1][i]
1296
- del my_network.network_lists[2][i]
1303
+ if my_network.network_lists is not None:
1304
+ for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
1305
+ if my_network.network_lists[0][i] in self.clicked_values['nodes'] or my_network.network_lists[0][i] in self.clicked_values['nodes']:
1306
+ del my_network.network_lists[0][i]
1307
+ del my_network.network_lists[1][i]
1308
+ del my_network.network_lists[2][i]
1297
1309
 
1298
1310
 
1299
1311
 
@@ -1303,11 +1315,12 @@ class ImageViewerWindow(QMainWindow):
1303
1315
  my_network.edges = my_network.edges * mask
1304
1316
  self.load_channel(1, my_network.edges, True)
1305
1317
 
1306
- for i in range(len(my_network.network_lists[1]) - 1, -1, -1):
1307
- if my_network.network_lists[2][i] in self.clicked_values['edges']:
1308
- del my_network.network_lists[0][i]
1309
- del my_network.network_lists[1][i]
1310
- del my_network.network_lists[2][i]
1318
+ if my_network.network_lists is not None:
1319
+ for i in range(len(my_network.network_lists[1]) - 1, -1, -1):
1320
+ if my_network.network_lists[2][i] in self.clicked_values['edges']:
1321
+ del my_network.network_lists[0][i]
1322
+ del my_network.network_lists[1][i]
1323
+ del my_network.network_lists[2][i]
1311
1324
 
1312
1325
  my_network.network_lists = my_network.network_lists
1313
1326
 
@@ -1451,6 +1464,7 @@ class ImageViewerWindow(QMainWindow):
1451
1464
  self.zoom_mode = self.zoom_button.isChecked()
1452
1465
  if self.zoom_mode:
1453
1466
  self.pan_button.setChecked(False)
1467
+ self.pen_button.setChecked(False)
1454
1468
  self.pan_mode = False
1455
1469
  self.brush_mode = False
1456
1470
  if self.machine_window is not None:
@@ -1468,6 +1482,7 @@ class ImageViewerWindow(QMainWindow):
1468
1482
  self.pan_mode = self.pan_button.isChecked()
1469
1483
  if self.pan_mode:
1470
1484
  self.zoom_button.setChecked(False)
1485
+ self.pen_button.setChecked(False)
1471
1486
  self.zoom_mode = False
1472
1487
  self.brush_mode = False
1473
1488
  if self.machine_window is not None:
@@ -1479,6 +1494,17 @@ class ImageViewerWindow(QMainWindow):
1479
1494
  else:
1480
1495
  self.machine_window.toggle_brush_button()
1481
1496
 
1497
+ def toggle_brush_mode(self):
1498
+ """Toggle brush mode on/off"""
1499
+ self.brush_mode = self.pen_button.isChecked()
1500
+ if self.brush_mode:
1501
+ self.pan_button.setChecked(False)
1502
+ self.zoom_button.setChecked(False)
1503
+ self.pan_mode = False
1504
+ self.zoom_mode = False
1505
+ self.update_brush_cursor()
1506
+ else:
1507
+ self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
1482
1508
 
1483
1509
 
1484
1510
  def on_mpl_scroll(self, event):
@@ -1666,7 +1692,10 @@ class ImageViewerWindow(QMainWindow):
1666
1692
  x, y = int(event.xdata), int(event.ydata)
1667
1693
  self.last_paint_pos = (x, y)
1668
1694
 
1669
- channel = 2
1695
+ if self.pen_button.isChecked():
1696
+ channel = self.active_channel
1697
+ else:
1698
+ channel = 2
1670
1699
 
1671
1700
  # Paint at initial position
1672
1701
  self.paint_at_position(x, y, self.erase, channel)
@@ -1705,6 +1734,8 @@ class ImageViewerWindow(QMainWindow):
1705
1734
 
1706
1735
  if erase:
1707
1736
  val = 0
1737
+ elif self.machine_window is None:
1738
+ val = 255
1708
1739
  elif self.foreground:
1709
1740
  val = 1
1710
1741
  else:
@@ -1789,13 +1820,16 @@ class ImageViewerWindow(QMainWindow):
1789
1820
 
1790
1821
  x, y = int(event.xdata), int(event.ydata)
1791
1822
 
1792
- channel = 2
1823
+ if self.pen_button.isChecked():
1824
+ channel = self.active_channel
1825
+ else:
1826
+ channel = 2
1793
1827
 
1794
1828
 
1795
- if self.channel_data[2] is not None:
1829
+ if self.channel_data[channel] is not None:
1796
1830
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
1797
1831
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
1798
- height, width = self.channel_data[2][self.current_slice].shape
1832
+ height, width = self.channel_data[channel][self.current_slice].shape
1799
1833
 
1800
1834
  if hasattr(self, 'last_paint_pos'):
1801
1835
  last_x, last_y = self.last_paint_pos
@@ -1888,8 +1922,11 @@ class ImageViewerWindow(QMainWindow):
1888
1922
 
1889
1923
  if self.brush_mode:
1890
1924
  self.painting = False
1891
- for i in self.restore_channels:
1892
- self.channel_visible[i] = True
1925
+ try:
1926
+ for i in self.restore_channels:
1927
+ self.channel_visible[i] = True
1928
+ except:
1929
+ pass
1893
1930
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
1894
1931
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
1895
1932
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
@@ -2223,6 +2260,8 @@ class ImageViewerWindow(QMainWindow):
2223
2260
  skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
2224
2261
  watershed_action = image_menu.addAction("Watershed")
2225
2262
  watershed_action.triggered.connect(self.show_watershed_dialog)
2263
+ invert_action = image_menu.addAction("Invert")
2264
+ invert_action.triggered.connect(self.show_invert_dialog)
2226
2265
  z_proj_action = image_menu.addAction("Z Project")
2227
2266
  z_proj_action.triggered.connect(self.show_z_dialog)
2228
2267
 
@@ -2254,8 +2293,6 @@ class ImageViewerWindow(QMainWindow):
2254
2293
  idoverlay_action = overlay_menu.addAction("Create ID Overlay")
2255
2294
  idoverlay_action.triggered.connect(self.show_idoverlay_dialog)
2256
2295
  searchoverlay_action = overlay_menu.addAction("Show Search Regions")
2257
- white_action = overlay_menu.addAction("White Background Overlay")
2258
- white_action.triggered.connect(self.show_white_dialog)
2259
2296
  searchoverlay_action.triggered.connect(self.show_search_dialog)
2260
2297
  shuffle_action = overlay_menu.addAction("Shuffle")
2261
2298
  shuffle_action.triggered.connect(self.show_shuffle_dialog)
@@ -2389,6 +2426,11 @@ class ImageViewerWindow(QMainWindow):
2389
2426
  dialog = WatershedDialog(self)
2390
2427
  dialog.exec()
2391
2428
 
2429
+ def show_invert_dialog(self):
2430
+ """Show the watershed parameter dialog."""
2431
+ dialog = InvertDialog(self)
2432
+ dialog.exec()
2433
+
2392
2434
  def show_z_dialog(self):
2393
2435
  """Show the z-proj dialog."""
2394
2436
  dialog = ZDialog(self)
@@ -2431,6 +2473,9 @@ class ImageViewerWindow(QMainWindow):
2431
2473
 
2432
2474
  def show_thresh_dialog(self):
2433
2475
  """Show threshold dialog"""
2476
+ if self.machine_window is not None:
2477
+ return
2478
+
2434
2479
  dialog = ThresholdDialog(self)
2435
2480
  dialog.exec()
2436
2481
 
@@ -2532,11 +2577,6 @@ class ImageViewerWindow(QMainWindow):
2532
2577
  dialog = SearchOverlayDialog(self)
2533
2578
  dialog.exec()
2534
2579
 
2535
- def show_white_dialog(self):
2536
- """Show the white dialog"""
2537
- dialog = WhiteDialog(self)
2538
- dialog.exec()
2539
-
2540
2580
  def show_shuffle_dialog(self):
2541
2581
  """Show the shuffle dialog"""
2542
2582
  dialog = ShuffleDialog(self)
@@ -4622,40 +4662,6 @@ class IdOverlayDialog(QDialog):
4622
4662
 
4623
4663
  self.accept()
4624
4664
 
4625
- class WhiteDialog(QDialog):
4626
-
4627
- def __init__(self, parent=None):
4628
-
4629
- super().__init__(parent)
4630
- self.setWindowTitle("Generate White Overlay?")
4631
- self.setModal(True)
4632
-
4633
- layout = QFormLayout(self)
4634
-
4635
- # Add Run button
4636
- run_button = QPushButton("Generate (Will go to Overlay 2)")
4637
- run_button.clicked.connect(self.white_overlay)
4638
- layout.addWidget(run_button)
4639
-
4640
- def white_overlay(self):
4641
-
4642
- try:
4643
- if isinstance(my_network.nodes, np.ndarray) :
4644
- overlay = np.ones_like(my_network.nodes).astype(np.uint8) * 255
4645
- elif isinstance(my_network.edges, np.ndarray):
4646
- overlay = np.ones_like(my_network.edges).astype(np.uint8) * 255
4647
- elif isinstance(my_network.network_overlay, np.ndarray):
4648
- overlay = np.ones_like(my_network.network_overlay).astype(np.uint8) * 255
4649
-
4650
- my_network.id_overlay = overlay
4651
-
4652
- self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
4653
-
4654
- self.accept()
4655
-
4656
- except Exception as e:
4657
- print(f"Error making white background: {e}")
4658
-
4659
4665
 
4660
4666
  class ShuffleDialog(QDialog):
4661
4667
 
@@ -5820,6 +5826,11 @@ class MachineWindow(QMainWindow):
5820
5826
 
5821
5827
  layout.addLayout(form_layout)
5822
5828
 
5829
+ if self.parent().pen_button.isChecked(): #Disable the pen mode if the user is in it because the segmenter pen forks it
5830
+ self.parent().pen_button.click()
5831
+
5832
+ self.parent().pen_button.setEnabled(False)
5833
+
5823
5834
 
5824
5835
  if self.parent().active_channel == 0:
5825
5836
  if self.parent().channel_data[0] is not None:
@@ -5848,56 +5859,85 @@ class MachineWindow(QMainWindow):
5848
5859
 
5849
5860
  self.parent().update_display()
5850
5861
 
5851
- # Set a reasonable default size
5852
- self.setMinimumWidth(400)
5853
- self.setMinimumHeight(400)
5862
+ # Set a reasonable default size for the window
5863
+ self.setMinimumWidth(600) # Increased to accommodate grouped buttons
5864
+ self.setMinimumHeight(500)
5854
5865
 
5855
- # Create zoom button and pan button
5856
- buttons_widget = QWidget()
5857
- buttons_layout = QHBoxLayout(buttons_widget)
5866
+ # Create main layout container
5867
+ main_widget = QWidget()
5868
+ main_layout = QVBoxLayout(main_widget)
5858
5869
 
5859
- # Create zoom button
5870
+ # Group 1: Drawing tools (Brush + Foreground/Background)
5871
+ drawing_group = QGroupBox("Drawing Tools")
5872
+ drawing_layout = QHBoxLayout()
5873
+
5874
+ # Brush button
5860
5875
  self.brush_button = QPushButton("🖌️")
5861
5876
  self.brush_button.setCheckable(True)
5862
5877
  self.brush_button.setFixedSize(40, 40)
5863
5878
  self.brush_button.clicked.connect(self.toggle_brush_mode)
5864
- form_layout.addWidget(self.brush_button)
5865
5879
  self.brush_button.click()
5866
5880
 
5881
+ # Foreground/Background buttons in their own horizontal layout
5882
+ fb_layout = QHBoxLayout()
5867
5883
  self.fore_button = QPushButton("Foreground")
5868
5884
  self.fore_button.setCheckable(True)
5869
5885
  self.fore_button.setChecked(True)
5870
5886
  self.fore_button.clicked.connect(self.toggle_foreground)
5871
- form_layout.addWidget(self.fore_button)
5872
5887
 
5873
5888
  self.back_button = QPushButton("Background")
5874
5889
  self.back_button.setCheckable(True)
5875
5890
  self.back_button.setChecked(False)
5876
5891
  self.back_button.clicked.connect(self.toggle_background)
5877
- form_layout.addWidget(self.back_button)
5878
5892
 
5893
+ fb_layout.addWidget(self.fore_button)
5894
+ fb_layout.addWidget(self.back_button)
5895
+
5896
+ drawing_layout.addWidget(self.brush_button)
5897
+ drawing_layout.addLayout(fb_layout)
5898
+ drawing_group.setLayout(drawing_layout)
5899
+
5900
+ # Group 2: Processing Options (GPU)
5901
+ processing_group = QGroupBox("Processing Options")
5902
+ processing_layout = QHBoxLayout()
5879
5903
  self.GPU = QPushButton("GPU")
5880
5904
  self.GPU.setCheckable(True)
5881
5905
  self.GPU.setChecked(False)
5882
5906
  self.GPU.clicked.connect(self.toggle_GPU)
5883
- form_layout.addWidget(self.GPU)
5884
5907
  self.use_gpu = False
5885
-
5886
- train_button = QPushButton("Train Quick Model")
5887
- train_button.clicked.connect(lambda: self.train_model(speed = True))
5888
- form_layout.addRow(train_button)
5889
-
5890
- train_button = QPushButton("Train More Detailed Model")
5891
- train_button.clicked.connect(lambda: self.train_model(speed = False))
5892
- form_layout.addRow(train_button)
5893
-
5908
+ processing_layout.addWidget(self.GPU)
5909
+ processing_group.setLayout(processing_layout)
5910
+
5911
+ # Group 3: Training Options
5912
+ training_group = QGroupBox("Training")
5913
+ training_layout = QVBoxLayout()
5914
+ train_quick = QPushButton("Train Quick Model")
5915
+ train_quick.clicked.connect(lambda: self.train_model(speed=True))
5916
+ train_detailed = QPushButton("Train More Detailed Model")
5917
+ train_detailed.clicked.connect(lambda: self.train_model(speed=False))
5918
+ training_layout.addWidget(train_quick)
5919
+ training_layout.addWidget(train_detailed)
5920
+ training_group.setLayout(training_layout)
5921
+
5922
+ # Group 4: Segmentation Options
5923
+ segmentation_group = QGroupBox("Segmentation")
5924
+ segmentation_layout = QVBoxLayout()
5894
5925
  seg_button = QPushButton("Preview Segment")
5895
5926
  seg_button.clicked.connect(self.start_segmentation)
5896
- form_layout.addRow(seg_button)
5897
-
5898
5927
  full_button = QPushButton("Segment All")
5899
5928
  full_button.clicked.connect(self.segment)
5900
- form_layout.addRow(full_button)
5929
+ segmentation_layout.addWidget(seg_button)
5930
+ segmentation_layout.addWidget(full_button)
5931
+ segmentation_group.setLayout(segmentation_layout)
5932
+
5933
+ # Add all groups to main layout
5934
+ main_layout.addWidget(drawing_group)
5935
+ main_layout.addWidget(processing_group)
5936
+ main_layout.addWidget(training_group)
5937
+ main_layout.addWidget(segmentation_group)
5938
+
5939
+ # Set the main widget as the central widget
5940
+ self.setCentralWidget(main_widget)
5901
5941
 
5902
5942
  self.trained = False
5903
5943
 
@@ -5988,7 +6028,25 @@ class MachineWindow(QMainWindow):
5988
6028
  self.segmenter.update_position(self.parent().current_slice, int((current_ylim[0] - current_ylim[1])/2), int((current_xlim[1] - current_xlim[0])/2))
5989
6029
  self.segmentation_worker.start()
5990
6030
 
6031
+ def confirm_seg_dialog(self):
6032
+ """Shows a dialog asking user to confirm segment all"""
6033
+ msg = QMessageBox()
6034
+ msg.setIcon(QMessageBox.Icon.Question)
6035
+ msg.setText("Alert")
6036
+ msg.setInformativeText("Segment Entire Image? (Window will freeze for processing)")
6037
+ msg.setWindowTitle("Confirm")
6038
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
6039
+ return msg.exec() == QMessageBox.StandardButton.Yes
5991
6040
 
6041
+ def confirm_close_dialog(self):
6042
+ """Shows a dialog asking user to confirm segment all"""
6043
+ msg = QMessageBox()
6044
+ msg.setIcon(QMessageBox.Icon.Question)
6045
+ msg.setText("Alert")
6046
+ msg.setInformativeText("Close Window?")
6047
+ msg.setWindowTitle("Confirm")
6048
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
6049
+ return msg.exec() == QMessageBox.StandardButton.Yes
5992
6050
 
5993
6051
  def update_display(self):
5994
6052
  if not hasattr(self, '_last_update'):
@@ -6030,6 +6088,8 @@ class MachineWindow(QMainWindow):
6030
6088
 
6031
6089
  if not self.trained:
6032
6090
  return
6091
+ elif not self.confirm_seg_dialog():
6092
+ return
6033
6093
  else:
6034
6094
  self.kill_segmentation()
6035
6095
  time.sleep(0.1)
@@ -6066,13 +6126,19 @@ class MachineWindow(QMainWindow):
6066
6126
  print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
6067
6127
 
6068
6128
  def closeEvent(self, event):
6069
- if self.brush_button.isChecked():
6070
- self.silence_button()
6071
- self.toggle_brush_mode()
6072
- self.parent().brush_mode = False
6073
- self.parent().machine_window = None
6074
- self.kill_segmentation()
6075
- time.sleep(0.1)
6129
+ if self.parent().isVisible():
6130
+ if self.confirm_close_dialog():
6131
+
6132
+ if self.brush_button.isChecked():
6133
+ self.silence_button()
6134
+ self.toggle_brush_mode()
6135
+ self.parent().pen_button.setEnabled(True)
6136
+ self.parent().brush_mode = False
6137
+ self.parent().machine_window = None
6138
+ self.kill_segmentation()
6139
+ time.sleep(0.1)
6140
+ else:
6141
+ event.ignore()
6076
6142
 
6077
6143
 
6078
6144
 
@@ -6946,6 +7012,65 @@ class WatershedDialog(QDialog):
6946
7012
  f"Error running watershed: {str(e)}"
6947
7013
  )
6948
7014
 
7015
+ class InvertDialog(QDialog):
7016
+
7017
+ def __init__(self, parent=None):
7018
+ super().__init__(parent)
7019
+ self.setWindowTitle("Invert Active Channel?")
7020
+ self.setModal(True)
7021
+
7022
+ layout = QFormLayout(self)
7023
+
7024
+ # Add Run button
7025
+ run_button = QPushButton("Run Invert")
7026
+ run_button.clicked.connect(self.run_invert)
7027
+ layout.addRow(run_button)
7028
+
7029
+ def run_invert(self):
7030
+
7031
+ try:
7032
+
7033
+ # Get the active channel data from parent
7034
+ active_data = self.parent().channel_data[self.parent().active_channel]
7035
+ if active_data is None:
7036
+ raise ValueError("No active image selected")
7037
+
7038
+ try:
7039
+ # Call binarize method with parameters
7040
+ if active_data.dtype == 'uint8' or 'int8':
7041
+ num = 255
7042
+ elif active_data.dtype == 'uint16' or 'int16':
7043
+ num = 65,535
7044
+ elif active_data.dtype == 'uint32' or 'int32':
7045
+ num = 2,147,483,647
7046
+
7047
+ result = (num - active_data
7048
+ )
7049
+
7050
+ # Update both the display data and the network object
7051
+ self.parent().channel_data[self.parent().active_channel] = result
7052
+
7053
+
7054
+ # Update the corresponding property in my_network
7055
+ setattr(my_network, network_properties[self.parent().active_channel], result)
7056
+
7057
+ self.parent().update_display()
7058
+ self.accept()
7059
+
7060
+ except Exception as e:
7061
+ QMessageBox.critical(
7062
+ self,
7063
+ "Error",
7064
+ f"Error running invert: {str(e)}"
7065
+ )
7066
+
7067
+ except Exception as e:
7068
+ QMessageBox.critical(
7069
+ self,
7070
+ "Error",
7071
+ f"Error running invert: {str(e)}"
7072
+ )
7073
+
6949
7074
  class ZDialog(QDialog):
6950
7075
 
6951
7076
  def __init__(self, parent=None):
@@ -7224,6 +7349,13 @@ class BranchDialog(QDialog):
7224
7349
  run_button.clicked.connect(self.branch_label)
7225
7350
  layout.addRow(run_button)
7226
7351
 
7352
+ if self.parent().channel_data[0] is not None:
7353
+ QMessageBox.critical(
7354
+ self,
7355
+ "Alert",
7356
+ "The nodes channel will be intermittently overwritten when running this method"
7357
+ )
7358
+
7227
7359
  def branch_label(self):
7228
7360
 
7229
7361
  try:
@@ -11,6 +11,7 @@ import concurrent.futures
11
11
  from concurrent.futures import ThreadPoolExecutor
12
12
  import threading
13
13
  from scipy import ndimage
14
+ import multiprocessing
14
15
 
15
16
 
16
17
  class InteractiveSegmenter:
@@ -18,11 +19,7 @@ class InteractiveSegmenter:
18
19
  self.image_3d = image_3d
19
20
  self.patterns = []
20
21
 
21
- try:
22
- self.use_gpu = use_gpu and cp.cuda.is_available()
23
- except:
24
- self.use_gpu = False
25
-
22
+ self.use_gpu = use_gpu and cp.cuda.is_available()
26
23
  if self.use_gpu:
27
24
  print(f"Using GPU: {torch.cuda.get_device_name()}")
28
25
  self.image_gpu = cp.asarray(image_3d)
@@ -350,23 +347,6 @@ class InteractiveSegmenter:
350
347
 
351
348
  return result
352
349
 
353
-
354
- def train(self):
355
- """Train random forest on accumulated patterns"""
356
- if len(self.patterns) < 2:
357
- return
358
-
359
- X = []
360
- y = []
361
- for pattern in self.patterns:
362
- X.extend(pattern['features'])
363
- y.extend([pattern['is_foreground']] * len(pattern['features']))
364
-
365
- X = np.array(X)
366
- y = np.array(y)
367
- self.model.fit(X, y)
368
- self.patterns = []
369
-
370
350
  def process_chunk_GPU(self, chunk_coords):
371
351
  """Process a chunk of coordinates using GPU acceleration"""
372
352
  coords = np.array(chunk_coords)
@@ -409,20 +389,40 @@ class InteractiveSegmenter:
409
389
 
410
390
  return foreground, background
411
391
 
412
- def segment_volume(self, chunk_size=32, gpu = False):
392
+ def segment_volume(self, chunk_size=64, gpu=False):
413
393
  """Segment volume using parallel processing of chunks with vectorized chunk creation"""
394
+ #Change the above chunk size to None to have it auto-compute largest chunks (not sure which is faster, 64 seems reasonable in test cases)
414
395
 
415
396
  try:
416
397
  from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
417
398
  except:
418
- print("Cannot find cuml, using CPU to segment instead...")
399
+ print("Cannot find cuML, using CPU to segment instead...")
419
400
  gpu = False
420
-
421
-
401
+
422
402
  if self.feature_cache is None:
423
403
  with self.lock:
424
404
  if self.feature_cache is None:
425
405
  self.feature_cache = self.compute_feature_maps()
406
+
407
+ print("Chunking data...")
408
+
409
+ # Determine optimal chunk size based on number of cores if not specified
410
+ if chunk_size is None:
411
+ total_cores = multiprocessing.cpu_count()
412
+
413
+ # Calculate total volume and target volume per core
414
+ total_volume = np.prod(self.image_3d.shape)
415
+ target_volume_per_chunk = total_volume / total_cores
416
+
417
+ # Calculate chunk size that would give us roughly one chunk per core
418
+ # Using cube root since we want roughly equal sizes in all dimensions
419
+ chunk_size = int(np.cbrt(target_volume_per_chunk))
420
+
421
+ # Ensure chunk size is at least 32 (minimum reasonable size) and not larger than smallest dimension
422
+ chunk_size = max(32, min(chunk_size, min(self.image_3d.shape)))
423
+
424
+ # Round to nearest multiple of 32 for better memory alignment
425
+ chunk_size = ((chunk_size + 15) // 32) * 32
426
426
 
427
427
  # Calculate number of chunks in each dimension
428
428
  z_chunks = (self.image_3d.shape[0] + chunk_size - 1) // chunk_size
@@ -455,6 +455,9 @@ class InteractiveSegmenter:
455
455
 
456
456
  foreground_coords = set()
457
457
  background_coords = set()
458
+
459
+ print("Segmenting chunks...")
460
+
458
461
 
459
462
  with ThreadPoolExecutor() as executor:
460
463
  if gpu:
@@ -482,7 +485,7 @@ class InteractiveSegmenter:
482
485
  self.current_y = y
483
486
 
484
487
 
485
- def get_realtime_chunks(self, chunk_size = 32):
488
+ def get_realtime_chunks(self, chunk_size = 64):
486
489
  print("Computing some overhead...")
487
490
 
488
491
 
@@ -553,7 +556,7 @@ class InteractiveSegmenter:
553
556
  try:
554
557
  from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
555
558
  except:
556
- print("Cannot find cuml, using CPU to segment instead...")
559
+ print("Cannot find cuML, using CPU to segment instead...")
557
560
  gpu = False
558
561
 
559
562
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: nettracer3d
3
- Version: 0.4.8
3
+ Version: 0.5.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
@@ -12,20 +12,20 @@ Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: numpy==1.26.4
15
- Requires-Dist: scipy
16
- Requires-Dist: scikit-image
17
- Requires-Dist: Pillow
18
- Requires-Dist: matplotlib
19
- Requires-Dist: networkx
20
- Requires-Dist: opencv-python-headless
21
- Requires-Dist: openpyxl
22
- Requires-Dist: pandas
23
- Requires-Dist: napari
24
- Requires-Dist: python-louvain
25
- Requires-Dist: tifffile
26
- Requires-Dist: qtrangeslider
27
- Requires-Dist: PyQt6
28
- Requires-Dist: scikit-learn
15
+ Requires-Dist: scipy==1.14.1
16
+ Requires-Dist: scikit-image==0.25.0
17
+ Requires-Dist: Pillow==11.1.0
18
+ Requires-Dist: matplotlib==3.9.2
19
+ Requires-Dist: networkx==3.2.1
20
+ Requires-Dist: opencv-python-headless==4.10.0.84
21
+ Requires-Dist: openpyxl==3.1.2
22
+ Requires-Dist: pandas==2.2.0
23
+ Requires-Dist: napari==0.5.5
24
+ Requires-Dist: python-louvain==0.16
25
+ Requires-Dist: tifffile==2023.7.18
26
+ Requires-Dist: qtrangeslider==0.1.5
27
+ Requires-Dist: PyQt6==6.8.0
28
+ Requires-Dist: scikit-learn==1.6.1
29
29
  Provides-Extra: cuda11
30
30
  Requires-Dist: cupy-cuda11x; extra == "cuda11"
31
31
  Provides-Extra: cuda12
@@ -0,0 +1,24 @@
1
+ numpy==1.26.4
2
+ scipy==1.14.1
3
+ scikit-image==0.25.0
4
+ Pillow==11.1.0
5
+ matplotlib==3.9.2
6
+ networkx==3.2.1
7
+ opencv-python-headless==4.10.0.84
8
+ openpyxl==3.1.2
9
+ pandas==2.2.0
10
+ napari==0.5.5
11
+ python-louvain==0.16
12
+ tifffile==2023.7.18
13
+ qtrangeslider==0.1.5
14
+ PyQt6==6.8.0
15
+ scikit-learn==1.6.1
16
+
17
+ [CUDA11]
18
+ cupy-cuda11x
19
+
20
+ [CUDA12]
21
+ cupy-cuda12x
22
+
23
+ [cupy]
24
+ cupy
@@ -1,24 +0,0 @@
1
- numpy==1.26.4
2
- scipy
3
- scikit-image
4
- Pillow
5
- matplotlib
6
- networkx
7
- opencv-python-headless
8
- openpyxl
9
- pandas
10
- napari
11
- python-louvain
12
- tifffile
13
- qtrangeslider
14
- PyQt6
15
- scikit-learn
16
-
17
- [CUDA11]
18
- cupy-cuda11x
19
-
20
- [CUDA12]
21
- cupy-cuda12x
22
-
23
- [cupy]
24
- cupy
File without changes
File without changes
File without changes