nettracer3d 0.9.5__py3-none-any.whl → 0.9.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QG
5
5
  QFormLayout, QLineEdit, QPushButton, QFileDialog,
6
6
  QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
7
7
  QMenu, QTabWidget, QGroupBox)
8
- from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent)
8
+ from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent, QEventLoop)
9
9
  import numpy as np
10
10
  import time
11
11
  from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
@@ -1002,7 +1002,15 @@ class ImageViewerWindow(QMainWindow):
1002
1002
  pass
1003
1003
 
1004
1004
  # Update display
1005
- self.update_display(preserve_zoom=(current_xlim, current_ylim), called = True)
1005
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
1006
+
1007
+ if self.pan_mode:
1008
+ self.create_pan_background()
1009
+ current_xlim = self.ax.get_xlim()
1010
+ current_ylim = self.ax.get_ylim()
1011
+ self.update_display_pan_mode(current_xlim, current_ylim)
1012
+
1013
+
1006
1014
 
1007
1015
  def create_mini_overlay(self, node_indices = None, edge_indices = None):
1008
1016
 
@@ -2473,7 +2481,7 @@ class ImageViewerWindow(QMainWindow):
2473
2481
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2474
2482
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2475
2483
 
2476
- if self.high_button.isChecked() and self.machine_window is None:
2484
+ if self.high_button.isChecked() and self.machine_window is None and not self.preview:
2477
2485
  if self.highlight_overlay is None and ((len(self.clicked_values['nodes']) + len(self.clicked_values['edges'])) > 0):
2478
2486
  if self.needs_mini:
2479
2487
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
@@ -4237,9 +4245,10 @@ class ImageViewerWindow(QMainWindow):
4237
4245
  load_action.triggered.connect(lambda: self.load_misc('Edge Centroids'))
4238
4246
  load_action = misc_menu.addAction("Load Node Communities")
4239
4247
  load_action.triggered.connect(lambda: self.load_misc('Communities'))
4240
- load_action = misc_menu.addAction("Merge Nodes")
4248
+ node_identities = file_menu.addMenu('Images -> Node Identities')
4249
+ load_action = node_identities.addAction("Merge Labeled Images Into Nodes")
4241
4250
  load_action.triggered.connect(lambda: self.load_misc('Merge Nodes'))
4242
- load_action = misc_menu.addAction("Merge Node IDs from Images")
4251
+ load_action = node_identities.addAction("Assign Node Identities From Overlap With Other Images")
4243
4252
  load_action.triggered.connect(self.show_merge_node_id_dialog)
4244
4253
 
4245
4254
 
@@ -4292,8 +4301,6 @@ class ImageViewerWindow(QMainWindow):
4292
4301
  id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
4293
4302
  umap_action = overlay_menu.addAction("Centroid UMAP")
4294
4303
  umap_action.triggered.connect(self.handle_centroid_umap)
4295
- iden_umap_action = overlay_menu.addAction("Identity UMAP (If any nodes were assigned multiple identities)")
4296
- iden_umap_action.triggered.connect(self.handle_iden_umap)
4297
4304
 
4298
4305
  rand_menu = analysis_menu.addMenu("Randomize")
4299
4306
  random_action = rand_menu.addAction("Generate Equivalent Random Network")
@@ -4363,7 +4370,7 @@ class ImageViewerWindow(QMainWindow):
4363
4370
  genvor_action = generate_menu.addAction("Generate Voronoi Diagram - goes in Overlay2")
4364
4371
  genvor_action.triggered.connect(self.voronoi)
4365
4372
 
4366
- modify_action = process_menu.addAction("Modify Network")
4373
+ modify_action = process_menu.addAction("Modify Network/Properties")
4367
4374
  modify_action.triggered.connect(self.show_modify_dialog)
4368
4375
 
4369
4376
 
@@ -4711,6 +4718,12 @@ class ImageViewerWindow(QMainWindow):
4711
4718
  # Create new table
4712
4719
  table = CustomTableView(self)
4713
4720
  table.setModel(PandasModel(df))
4721
+
4722
+ try:
4723
+ first_column_name = table.model()._data.columns[0]
4724
+ table.sort_table(first_column_name, ascending=True)
4725
+ except:
4726
+ pass
4714
4727
 
4715
4728
  # Add to tabbed widget
4716
4729
  if title is None:
@@ -4718,17 +4731,27 @@ class ImageViewerWindow(QMainWindow):
4718
4731
  else:
4719
4732
  self.tabbed_data.add_table(f"{title}", table)
4720
4733
 
4734
+
4735
+
4721
4736
  # Adjust column widths to content
4722
4737
  for column in range(table.model().columnCount(None)):
4723
4738
  table.resizeColumnToContents(column)
4724
4739
 
4725
4740
  except:
4726
- pass
4741
+ pass
4727
4742
 
4728
4743
  def show_merge_node_id_dialog(self):
4729
4744
 
4730
- dialog = MergeNodeIdDialog(self)
4731
- dialog.exec()
4745
+ if my_network.nodes is None:
4746
+ QMessageBox.critical(
4747
+ self,
4748
+ "Error",
4749
+ "Please load your segmented cells into 'Nodes' channel first"
4750
+ )
4751
+ return
4752
+ else:
4753
+ dialog = MergeNodeIdDialog(self)
4754
+ dialog.exec()
4732
4755
 
4733
4756
  def show_gray_water_dialog(self):
4734
4757
  """Show the gray watershed parameter dialog."""
@@ -5095,29 +5118,6 @@ class ImageViewerWindow(QMainWindow):
5095
5118
  if sort == 'Node Identities':
5096
5119
  my_network.load_node_identities(file_path = filename)
5097
5120
 
5098
- """
5099
- first_value = list(my_network.node_identities.values())[0] # Check that there are not multiple IDs
5100
- if isinstance(first_value, (list, tuple)):
5101
- trump_value, ok = QInputDialog.getText(
5102
- self,
5103
- 'Multiple IDs Detected',
5104
- 'The node identities appear to contain multiple ids per node in a list.\n'
5105
- 'If you desire one node ID to trump all others, enter it here.\n'
5106
- '(Enter "-" to have the first IDs trump all others)\n'
5107
- '(Enter "/" to have multi-ID nodes be split into many nodes sharing a centroid)\n'
5108
- '(Close this window to continue with multi-ID nodes)'
5109
- )
5110
- if not ok or trump_value.strip() == '':
5111
- trump_value = None
5112
- elif trump_value.upper() == '-':
5113
- trump_value = '-'
5114
- elif trump_value.upper() == "/":
5115
- trump_value = '/'
5116
- my_network.node_identities = uncork(my_network.node_identities, trump_value)
5117
- else:
5118
- trump_value = None
5119
- my_network.node_identities = uncork(my_network.node_identities, trump_value)
5120
- """
5121
5121
 
5122
5122
  if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
5123
5123
  try:
@@ -5166,6 +5166,14 @@ class ImageViewerWindow(QMainWindow):
5166
5166
  elif sort == 'Merge Nodes':
5167
5167
  try:
5168
5168
 
5169
+ if my_network.nodes is None:
5170
+ QMessageBox.critical(
5171
+ self,
5172
+ "Error",
5173
+ "Please load your first set of nodes into the 'Nodes' channel first"
5174
+ )
5175
+ return
5176
+
5169
5177
  if len(np.unique(my_network.nodes)) < 3:
5170
5178
  self.show_label_dialog()
5171
5179
 
@@ -5179,6 +5187,21 @@ class ImageViewerWindow(QMainWindow):
5179
5187
 
5180
5188
  msg.exec()
5181
5189
 
5190
+ # Also if they want centroids:
5191
+ msg2 = QMessageBox()
5192
+ msg2.setWindowTitle("Selection Type")
5193
+ msg2.setText("Would you like to compute node centroids for each image prior to merging?")
5194
+ yes_button = msg2.addButton("Yes", QMessageBox.ButtonRole.AcceptRole)
5195
+ no_button = msg2.addButton("No", QMessageBox.ButtonRole.AcceptRole)
5196
+ msg2.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
5197
+
5198
+ msg2.exec()
5199
+
5200
+ if msg2.clickedButton() == yes_button:
5201
+ centroids = True
5202
+ else:
5203
+ centroids = False
5204
+
5182
5205
  if msg.clickedButton() == tiff_button:
5183
5206
  # Code for selecting TIFF files
5184
5207
  filename, _ = QFileDialog.getOpenFileName(
@@ -5201,7 +5224,7 @@ class ImageViewerWindow(QMainWindow):
5201
5224
  if dialog.exec() == QFileDialog.DialogCode.Accepted:
5202
5225
  selected_path = dialog.directory().absolutePath()
5203
5226
 
5204
- my_network.merge_nodes(selected_path, root_id = self.node_name)
5227
+ my_network.merge_nodes(selected_path, root_id = self.node_name, centroids = centroids)
5205
5228
  self.load_channel(0, my_network.nodes, True)
5206
5229
 
5207
5230
 
@@ -5210,8 +5233,12 @@ class ImageViewerWindow(QMainWindow):
5210
5233
  self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
5211
5234
  except Exception as e:
5212
5235
  print(f"Error loading node identity table: {e}")
5236
+ if centroids:
5237
+ self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
5238
+
5213
5239
 
5214
5240
  except Exception as e:
5241
+
5215
5242
  QMessageBox.critical(
5216
5243
  self,
5217
5244
  "Error Merging",
@@ -5729,6 +5756,8 @@ class ImageViewerWindow(QMainWindow):
5729
5756
  except:
5730
5757
  pass
5731
5758
 
5759
+ if self.shape == self.channel_data[channel_index].shape:
5760
+ preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
5732
5761
  self.shape = self.channel_data[channel_index].shape
5733
5762
  if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
5734
5763
  self.throttle = True
@@ -5820,7 +5849,7 @@ class ImageViewerWindow(QMainWindow):
5820
5849
  # Update display
5821
5850
  self.update_display(preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim()))
5822
5851
 
5823
- def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False, update = True):
5852
+ def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False, update = True, node_identities = False):
5824
5853
  """Method to flexibly reset certain fields to free up the RAM as desired"""
5825
5854
 
5826
5855
  # Set scales first before any clearing operations
@@ -5840,6 +5869,9 @@ class ImageViewerWindow(QMainWindow):
5840
5869
  # Clear selection table
5841
5870
  self.selection_table.setModel(PandasModel(empty_df))
5842
5871
 
5872
+ if node_identities:
5873
+ my_network.node_identities = None
5874
+
5843
5875
  if nodes:
5844
5876
  self.delete_channel(0, False, update = update)
5845
5877
 
@@ -5990,7 +6022,9 @@ class ImageViewerWindow(QMainWindow):
5990
6022
  # Now convert to real data
5991
6023
  self.pm.convert_virtual_strokes_to_data()
5992
6024
  self.current_slice = slice_value
5993
- if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
6025
+ if self.preview:
6026
+ self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
6027
+ elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
5994
6028
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
5995
6029
  if not self.hold_update:
5996
6030
  self.update_display(preserve_zoom=view_settings)
@@ -6250,8 +6284,8 @@ class ImageViewerWindow(QMainWindow):
6250
6284
  vmin=0, vmax=1, extent=crop_extent)
6251
6285
 
6252
6286
  # Handle preview, overlays, and measurements (apply cropping here too)
6253
- if self.preview and not called:
6254
- self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
6287
+ #if self.preview and not called:
6288
+ # self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
6255
6289
 
6256
6290
  # Overlay handling (optimized with cropping and downsampling)
6257
6291
  if self.mini_overlay and self.highlight and self.machine_window is None:
@@ -6465,12 +6499,6 @@ class ImageViewerWindow(QMainWindow):
6465
6499
 
6466
6500
  my_network.centroid_umap()
6467
6501
 
6468
- def handle_iden_umap(self):
6469
-
6470
- if my_network.node_identities is None:
6471
- return
6472
-
6473
- my_network.identity_umap()
6474
6502
 
6475
6503
  def closeEvent(self, event):
6476
6504
  """Override closeEvent to close all windows when main window closes"""
@@ -6487,6 +6515,8 @@ class ImageViewerWindow(QMainWindow):
6487
6515
  # Force quit the application
6488
6516
  QCoreApplication.quit()
6489
6517
 
6518
+ exit()
6519
+
6490
6520
 
6491
6521
 
6492
6522
  #TABLE RELATED:
@@ -6729,11 +6759,7 @@ class CustomTableView(QTableView):
6729
6759
  self.resizeColumnToContents(col)
6730
6760
 
6731
6761
  except Exception as e:
6732
- QMessageBox.critical(
6733
- self,
6734
- "Error",
6735
- f"Error sorting table: {str(e)}"
6736
- )
6762
+ pass
6737
6763
 
6738
6764
  def save_table_as(self, file_type):
6739
6765
  """Save the table data as either CSV or Excel file."""
@@ -7253,8 +7279,13 @@ class PropertiesDialog(QDialog):
7253
7279
  self.network.setChecked(self.check_checked(my_network.network))
7254
7280
  layout.addRow("Network Status", self.network)
7255
7281
 
7282
+ self.node_identities = QPushButton("Node Identities")
7283
+ self.node_identities.setCheckable(True)
7284
+ self.node_identities.setChecked(self.check_checked(my_network.node_identities))
7285
+ layout.addRow("Identities Status", self.node_identities)
7286
+
7256
7287
  # Add Run button
7257
- run_button = QPushButton("Enter")
7288
+ run_button = QPushButton("Enter (Erases Unchecked Properties)")
7258
7289
  run_button.clicked.connect(self.run_properties)
7259
7290
  layout.addWidget(run_button)
7260
7291
 
@@ -7291,8 +7322,9 @@ class PropertiesDialog(QDialog):
7291
7322
  id_overlay = not self.id_overlay.isChecked()
7292
7323
  search_region = not self.search_region.isChecked()
7293
7324
  network = not self.network.isChecked()
7325
+ node_identities = not self.node_identities.isChecked()
7294
7326
 
7295
- self.parent().reset(nodes = nodes, edges = edges, search_region = search_region, network_overlay = network_overlay, id_overlay = id_overlay, network = network, xy_scale = xy_scale, z_scale = z_scale)
7327
+ self.parent().reset(nodes = nodes, edges = edges, search_region = search_region, network_overlay = network_overlay, id_overlay = id_overlay, network = network, xy_scale = xy_scale, z_scale = z_scale, node_identities = node_identities)
7296
7328
 
7297
7329
  self.accept()
7298
7330
 
@@ -7720,78 +7752,252 @@ class ArbitraryDialog(QDialog):
7720
7752
  except Exception as e:
7721
7753
  QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
7722
7754
 
7723
- class MergeNodeIdDialog(QDialog):
7724
7755
 
7756
+ class MergeNodeIdDialog(QDialog):
7725
7757
  def __init__(self, parent=None):
7758
+
7726
7759
  super().__init__(parent)
7760
+
7727
7761
  self.setWindowTitle("Merging Node Identities From Folder Dialog.\nNote that you should prelabel or prewatershed your current node objects before doing this. (See Process -> Image) It does not label them for you.")
7728
7762
  self.setModal(True)
7729
7763
 
7730
7764
  layout = QFormLayout(self)
7731
-
7732
7765
  self.search = QLineEdit("")
7733
7766
  layout.addRow("Step-out distance (from current nodes image - ignore if you dilated them previously or don't want):", self.search)
7734
-
7735
7767
  self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
7736
7768
  layout.addRow("xy_scale:", self.xy_scale)
7737
-
7738
7769
  self.z_scale = QLineEdit(f"{my_network.z_scale}")
7739
7770
  layout.addRow("z_scale:", self.z_scale)
7771
+ self.mode_selector = QComboBox()
7772
+ self.mode_selector.addItems(["Auto-Binarize(Otsu)/Presegmented", "Manual (Interactive Thresholder)"])
7773
+ self.mode_selector.setCurrentIndex(1) # Default to Mode 1
7774
+ layout.addRow("Binarization Strategy:", self.mode_selector)
7740
7775
 
7741
- # Add Run button
7742
- self.include = QPushButton("Include Negative Gates?")
7776
+ self.umap = QPushButton("If using manual threshold: Also generate identity UMAP?")
7777
+ self.umap.setCheckable(True)
7778
+ self.umap.setChecked(True)
7779
+ layout.addWidget(self.umap)
7780
+
7781
+ self.include = QPushButton("Include When a Node is Negative for an ID?")
7743
7782
  self.include.setCheckable(True)
7744
- self.include.setChecked(True)
7783
+ self.include.setChecked(False)
7745
7784
  layout.addWidget(self.include)
7746
-
7747
- # Add Run button
7785
+
7748
7786
  run_button = QPushButton("Get Directory")
7749
7787
  run_button.clicked.connect(self.run)
7750
7788
  layout.addWidget(run_button)
7751
7789
 
7752
- def run(self):
7790
+ def wait_for_threshold_processing(self):
7791
+ """
7792
+ Opens ThresholdWindow and waits for user to process the image.
7793
+ Returns True if completed, False if cancelled.
7794
+ The thresholded image will be available in the main window after completion.
7795
+ """
7796
+ # Create event loop to wait for user
7797
+ loop = QEventLoop()
7798
+ result = {'completed': False}
7799
+
7800
+ # Create the threshold window
7801
+ thresh_window = ThresholdWindow(self.parent(), 4)
7802
+
7803
+ # Connect signals
7804
+ def on_processing_complete():
7805
+ result['completed'] = True
7806
+ loop.quit()
7807
+
7808
+ def on_processing_cancelled():
7809
+ result['completed'] = False
7810
+ loop.quit()
7811
+
7812
+ thresh_window.processing_complete.connect(on_processing_complete)
7813
+ thresh_window.processing_cancelled.connect(on_processing_cancelled)
7814
+
7815
+ # Show window and wait
7816
+ thresh_window.show()
7817
+ thresh_window.raise_()
7818
+ thresh_window.activateWindow()
7819
+
7820
+ # Block until user clicks "Apply Threshold & Continue" or "Cancel"
7821
+ loop.exec()
7822
+
7823
+ # Clean up
7824
+ thresh_window.deleteLater()
7825
+
7826
+ return result['completed']
7753
7827
 
7828
+ def run(self):
7754
7829
  try:
7755
7830
 
7756
7831
  search = float(self.search.text()) if self.search.text().strip() else 0
7757
7832
  xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
7758
7833
  z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
7759
-
7760
-
7761
7834
  data = self.parent().channel_data[0]
7762
7835
  include = self.include.isChecked()
7763
-
7836
+ umap = self.umap.isChecked()
7837
+
7764
7838
  if data is None:
7765
7839
  return
7766
-
7767
-
7768
-
7840
+
7769
7841
  dialog = QFileDialog(self)
7770
7842
  dialog.setOption(QFileDialog.Option.DontUseNativeDialog)
7771
7843
  dialog.setOption(QFileDialog.Option.ReadOnly)
7772
7844
  dialog.setFileMode(QFileDialog.FileMode.Directory)
7773
7845
  dialog.setViewMode(QFileDialog.ViewMode.Detail)
7774
-
7846
+
7775
7847
  if dialog.exec() == QFileDialog.DialogCode.Accepted:
7776
7848
  selected_path = dialog.directory().absolutePath()
7777
-
7849
+ else:
7850
+ return # User cancelled directory selection
7851
+
7778
7852
  if search > 0:
7779
- data = sdl.smart_dilate(data, 1, 1, GPU = False, fast_dil = False, use_dt_dil_amount = search, xy_scale = xy_scale, z_scale = z_scale)
7853
+ data = sdl.smart_dilate(data, 1, 1, GPU=False, fast_dil=False,
7854
+ use_dt_dil_amount=search, xy_scale=xy_scale, z_scale=z_scale)
7855
+
7856
+ # Check if manual mode is selected
7857
+ if self.mode_selector.currentIndex() == 1: # Manual mode
7780
7858
 
7781
- my_network.merge_node_ids(selected_path, data, include)
7859
+ if my_network.node_identities is None: # Prepare modular dict
7782
7860
 
7783
- self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
7861
+ my_network.node_identities = {}
7784
7862
 
7785
- QMessageBox.information(
7786
- self,
7787
- "Success",
7788
- "Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
7789
- )
7863
+ nodes = list(np.unique(data))
7864
+ if 0 in nodes:
7865
+ del nodes[0]
7866
+ for node in nodes:
7867
+
7868
+ my_network.node_identities[node] = [] # Assign to lists at first
7869
+ else:
7870
+ for node, iden in my_network.node_identities.items():
7871
+ try:
7872
+ my_network.node_identities[node] = ast.literal_eval(iden)
7873
+ except:
7874
+ my_network.node_identities[node] = [iden]
7875
+
7876
+ id_dicts = my_network.get_merge_node_dictionaries(selected_path, data)
7877
+
7878
+ # For loop example - get threshold for multiple images/data
7879
+ results = []
7880
+
7881
+ img_list = n3d.directory_info(selected_path)
7882
+ data_backup = copy.deepcopy(data)
7883
+ self.parent().load_channel(0, data, data = True)
7884
+ self.hide()
7885
+ self.parent().highlight_overlay = None
7886
+
7887
+ good_list = []
7888
+
7889
+ for i, img in enumerate(img_list):
7890
+
7891
+ if img.endswith('.tiff') or img.endswith('.tif'):
7892
+
7893
+ print(f"Please threshold {img}")
7894
+
7895
+
7896
+ mask = tifffile.imread(f'{selected_path}/{img}')
7897
+ self.parent().load_channel(2, mask, data = True)
7898
+
7899
+ # Wait for user to threshold this data
7900
+ self.parent().special_dict = id_dicts[i]
7901
+ processing_completed = self.wait_for_threshold_processing()
7902
+
7903
+ if not processing_completed:
7904
+ # User cancelled, ask if they want to continue
7905
+ reply = QMessageBox.question(self, 'Continue?',
7906
+ f'Threshold cancelled for item {i+1}. Continue with remaining items?',
7907
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
7908
+ if reply == QMessageBox.StandardButton.No:
7909
+ break
7910
+ continue
7911
+
7912
+ # At this point, the thresholded image is in the main window's memory
7913
+ # Get the processed/thresholded data from wherever ThresholdWindow stored it
7914
+ thresholded_vals = list(np.unique(self.parent().channel_data[0]))
7915
+ if 0 in thresholded_vals:
7916
+ del thresholded_vals[0]
7917
+
7918
+ if img.endswith('.tiff'):
7919
+ base_name = img[:-5]
7920
+ elif img.endswith('.tif'):
7921
+ base_name = img[:-4]
7922
+ else:
7923
+ base_name = img
7924
+
7925
+ assigned = {}
7926
+
7927
+ for node in my_network.node_identities.keys():
7928
+
7929
+ try:
7930
+
7931
+ if int(node) in thresholded_vals:
7932
+
7933
+ my_network.node_identities[node].append(f'{base_name}+')
7934
+
7935
+ elif include:
7936
+
7937
+ my_network.node_identities[node].append(f'{base_name}-')
7938
+
7939
+ except:
7940
+ pass
7941
+
7942
+ # Process the thresholded data
7943
+ self.parent().highlight_overlay = None
7944
+ self.parent().load_channel(0, data_backup, data = True)
7945
+ good_list.append(base_name)
7946
+
7947
+ modify_dict = copy.deepcopy(my_network.node_identities)
7948
+
7949
+ for node, iden in my_network.node_identities.items():
7950
+
7951
+ try:
7952
+
7953
+ if len(iden) == 1:
7954
+
7955
+ modify_dict[node] = str(iden[0]) # Singleton lists become bare strings
7956
+ elif len(iden) == 0:
7957
+ del modify_dict[node]
7958
+ else:
7959
+ modify_dict[node] = str(iden) # We hold multi element lists as strings for compatibility
7960
+
7961
+ except:
7962
+ pass
7963
+
7964
+ my_network.node_identities = modify_dict
7965
+
7966
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
7967
+
7968
+ all_keys = id_dicts[0].keys()
7969
+ result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
7970
+
7971
+
7972
+ self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity')
7973
+ if umap:
7974
+ my_network.identity_umap(result)
7790
7975
 
7791
- self.accept()
7976
+
7977
+ QMessageBox.information(
7978
+ self,
7979
+ "Success",
7980
+ "Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
7981
+ )
7982
+
7983
+ self.accept()
7984
+ else:
7985
+ my_network.merge_node_ids(selected_path, data, include)
7986
+
7987
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
7988
+
7989
+ QMessageBox.information(
7990
+ self,
7991
+ "Success",
7992
+ "Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
7993
+ )
7994
+
7995
+ self.accept()
7792
7996
 
7793
7997
  except Exception as e:
7794
- print(f"Error: {e}")
7998
+ import traceback
7999
+ print(traceback.format_exc())
8000
+ #print(f"Error: {e}")
7795
8001
 
7796
8002
 
7797
8003
  class Show3dDialog(QDialog):
@@ -11172,11 +11378,15 @@ class SegmentationWorker(QThread):
11172
11378
 
11173
11379
 
11174
11380
  class ThresholdWindow(QMainWindow):
11381
+ processing_complete = pyqtSignal() # Emitted when user finishes and images are modified
11382
+ processing_cancelled = pyqtSignal() # Emitted when user cancels
11383
+
11175
11384
  def __init__(self, parent=None, accepted_mode=0):
11176
11385
  super().__init__(parent)
11177
11386
  self.setWindowTitle("Threshold")
11178
11387
 
11179
11388
  self.accepted_mode = accepted_mode
11389
+ self.preview = True
11180
11390
 
11181
11391
  # Create central widget and layout
11182
11392
  central_widget = QWidget()
@@ -11208,6 +11418,10 @@ class ThresholdWindow(QMainWindow):
11208
11418
  self.histo_list = list(self.parent().degree_dict.values())
11209
11419
  self.bounds = False
11210
11420
  self.parent().bounds = False
11421
+ elif accepted_mode == 4:
11422
+ self.histo_list = list(self.parent().special_dict.values())
11423
+ self.bounds = False
11424
+ self.parent().bounds = False
11211
11425
 
11212
11426
  elif accepted_mode == 0:
11213
11427
  targ_shape = self.parent().channel_data[self.parent().active_channel].shape
@@ -11279,16 +11493,39 @@ class ThresholdWindow(QMainWindow):
11279
11493
  self.preview.clicked.connect(self.preview_mode)
11280
11494
  form_layout.addRow("Show Preview:", self.preview)
11281
11495
 
11282
- run_button = QPushButton("Apply Threshold")
11283
- run_button.clicked.connect(self.thresh)
11284
- form_layout.addRow(run_button)
11496
+ button_layout = QHBoxLayout()
11497
+
11498
+
11499
+ # Keep your existing Apply Threshold button, but modify its behavior
11500
+ run_button = QPushButton("Apply Threshold/Continue")
11501
+ run_button.clicked.connect(self.apply_and_continue) # New method
11502
+ button_layout.addWidget(run_button)
11285
11503
 
11286
- layout.addLayout(form_layout)
11504
+ # Add Cancel button for external dialog use
11505
+ cancel_button = QPushButton("Cancel/Skip")
11506
+ cancel_button.clicked.connect(self.cancel_processing)
11507
+ button_layout.addWidget(cancel_button)
11287
11508
 
11509
+ form_layout.addRow(button_layout)
11510
+ layout.addLayout(form_layout)
11511
+
11288
11512
  # Set a reasonable default size
11289
11513
  self.setMinimumWidth(400)
11290
11514
  self.setMinimumHeight(400)
11291
11515
 
11516
+ def apply_and_continue(self):
11517
+ """Apply threshold, modify main window images, then signal completion"""
11518
+ self.thresh() # This should modify the main window images
11519
+
11520
+ # Signal that processing is complete
11521
+ self.processing_complete.emit()
11522
+ self.close()
11523
+
11524
+ def cancel_processing(self):
11525
+ """Cancel without applying changes"""
11526
+ self.processing_cancelled.emit()
11527
+ self.close()
11528
+
11292
11529
  def closeEvent(self, event):
11293
11530
  self.parent().preview = False
11294
11531
  self.parent().targs = None
@@ -11340,6 +11577,10 @@ class ThresholdWindow(QMainWindow):
11340
11577
  for node, vol in self.parent().degree_dict.items():
11341
11578
  if min_val <= vol <= max_val:
11342
11579
  output.append(node)
11580
+ elif self.accepted_mode == 4:
11581
+ for node, vol in self.parent().special_dict.items():
11582
+ if min_val <= vol <= max_val:
11583
+ output.append(node)
11343
11584
  return output
11344
11585
 
11345
11586
  def get_values_in_range(self, lst, min_val, max_val):
@@ -11357,11 +11598,18 @@ class ThresholdWindow(QMainWindow):
11357
11598
  for item in self.parent().degree_dict:
11358
11599
  if self.parent().degree_dict[item] in values:
11359
11600
  output.append(item)
11601
+ elif self.accepted_mode == 4:
11602
+ for item in self.parent().special_dict:
11603
+ if self.parent().special_dict[item] in values:
11604
+ output.append(item)
11605
+
11360
11606
  return output
11361
11607
 
11362
11608
 
11363
11609
  def min_value_changed(self):
11364
11610
  try:
11611
+ if not self.preview.isChecked():
11612
+ self.preview.click()
11365
11613
  text = self.min.text()
11366
11614
  if not text: # If empty, ignore
11367
11615
  return
@@ -11409,6 +11657,8 @@ class ThresholdWindow(QMainWindow):
11409
11657
 
11410
11658
  def max_value_changed(self):
11411
11659
  try:
11660
+ if not self.preview.isChecked():
11661
+ self.preview.click()
11412
11662
  text = self.max.text()
11413
11663
  if not text: # If empty, ignore
11414
11664
  return
@@ -11532,7 +11782,6 @@ class ThresholdWindow(QMainWindow):
11532
11782
  f"Error running threshold: {str(e)}"
11533
11783
  )
11534
11784
 
11535
-
11536
11785
  class SmartDilateDialog(QDialog):
11537
11786
  def __init__(self, parent, params):
11538
11787
  super().__init__(parent)
@@ -13191,6 +13440,12 @@ class ModifyDialog(QDialog):
13191
13440
  self.revid.setChecked(False)
13192
13441
  layout.addRow("Remove Unassigned IDs from Centroid List?:", self.revid)
13193
13442
 
13443
+ self.revdupeid = QPushButton("Make Singleton IDs")
13444
+ self.revdupeid.setCheckable(True)
13445
+ self.revdupeid.setChecked(False)
13446
+ layout.addRow("Force Any Multiple IDs to Pick a Random Single ID?:", self.revdupeid)
13447
+
13448
+
13194
13449
  self.remove = QPushButton("Remove Missing")
13195
13450
  self.remove.setCheckable(True)
13196
13451
  self.remove.setChecked(False)
@@ -13269,6 +13524,7 @@ class ModifyDialog(QDialog):
13269
13524
  try:
13270
13525
 
13271
13526
  revid = self.revid.isChecked()
13527
+ revdupeid = self.revdupeid.isChecked()
13272
13528
  trunk = self.trunk.isChecked()
13273
13529
  if not trunk:
13274
13530
  trunknode = self.trunknode.isChecked()
@@ -13293,6 +13549,20 @@ class ModifyDialog(QDialog):
13293
13549
  except:
13294
13550
  pass
13295
13551
 
13552
+ if revdupeid:
13553
+ try:
13554
+ for node, iden in my_network.node_identities.items():
13555
+ try:
13556
+ import ast
13557
+ import random
13558
+ iden = ast.literal_eval(iden)
13559
+ my_network.node_identities[node] = random.choice(iden)
13560
+ except:
13561
+ pass
13562
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
13563
+ except:
13564
+ pass
13565
+
13296
13566
 
13297
13567
  if remove:
13298
13568
  my_network.purge_properties()