nettracer3d 0.7.6__py3-none-any.whl → 0.7.7__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)
8
+ from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication)
9
9
  import numpy as np
10
10
  import time
11
11
  from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
@@ -26,6 +26,7 @@ from concurrent.futures import ThreadPoolExecutor
26
26
  from functools import partial
27
27
  from nettracer3d import segmenter
28
28
  from nettracer3d import segmenter_GPU
29
+ from nettracer3d import excelotron
29
30
 
30
31
 
31
32
 
@@ -409,6 +410,11 @@ class ImageViewerWindow(QMainWindow):
409
410
  self.mini_overlay = False # If the program is currently drawing the overlay by frame this will be true
410
411
  self.mini_overlay_data = None #Actual data for mini overlay
411
412
  self.mini_thresh = (500*500*500) # Array volume to start using mini overlays for
413
+ self.shape = None
414
+
415
+ self.excel_manager = ExcelotronManager(self)
416
+ self.excel_manager.data_received.connect(self.handle_excel_data)
417
+ self.prev_coms = None
412
418
 
413
419
  def start_left_scroll(self):
414
420
  """Start scrolling left when left arrow is pressed."""
@@ -2680,6 +2686,8 @@ class ImageViewerWindow(QMainWindow):
2680
2686
  load_action.triggered.connect(lambda checked, ch=i: self.load_channel(ch))
2681
2687
  load_action = load_menu.addAction("Load Network")
2682
2688
  load_action.triggered.connect(self.load_network)
2689
+ load_action = load_menu.addAction("Load From Excel Helper")
2690
+ load_action.triggered.connect(self.launch_excelotron)
2683
2691
  misc_menu = load_menu.addMenu("Load Misc Properties")
2684
2692
  load_action = misc_menu.addAction("Load Node IDs")
2685
2693
  load_action.triggered.connect(lambda: self.load_misc('Node Identities'))
@@ -2696,10 +2704,14 @@ class ImageViewerWindow(QMainWindow):
2696
2704
  network_menu = analysis_menu.addMenu("Network")
2697
2705
  netshow_action = network_menu.addAction("Show Network")
2698
2706
  netshow_action.triggered.connect(self.show_netshow_dialog)
2699
- partition_action = network_menu.addAction("Community Partition +Generic Community Stats")
2707
+ partition_action = network_menu.addAction("Community Partition + Generic Community Stats")
2700
2708
  partition_action.triggered.connect(self.show_partition_dialog)
2701
- com_identity_action = network_menu.addAction("Identity Makeup of Network Communities (Weighted avg by community size)")
2709
+ com_identity_action = network_menu.addAction("Identity Makeup of Network Communities (and UMAP)")
2702
2710
  com_identity_action.triggered.connect(self.handle_com_id)
2711
+ com_neighbor_action = network_menu.addAction("Convert Network Communities into Neighborhoods?")
2712
+ com_neighbor_action.triggered.connect(self.handle_com_neighbor)
2713
+ com_cell_action = network_menu.addAction("Create Communities Based on Cuboidal Proximity Cells?")
2714
+ com_cell_action.triggered.connect(self.handle_com_cell)
2703
2715
  stats_menu = analysis_menu.addMenu("Stats")
2704
2716
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
2705
2717
  allstats_action.triggered.connect(self.stats)
@@ -2768,6 +2780,8 @@ class ImageViewerWindow(QMainWindow):
2768
2780
  thresh_action.triggered.connect(self.show_thresh_dialog)
2769
2781
  mask_action = image_menu.addAction("Mask Channel")
2770
2782
  mask_action.triggered.connect(self.show_mask_dialog)
2783
+ crop_action = image_menu.addAction("Crop Channels")
2784
+ crop_action.triggered.connect(self.show_crop_dialog)
2771
2785
  type_action = image_menu.addAction("Channel dtype")
2772
2786
  type_action.triggered.connect(self.show_type_dialog)
2773
2787
  skeletonize_action = image_menu.addAction("Skeletonize")
@@ -3135,6 +3149,11 @@ class ImageViewerWindow(QMainWindow):
3135
3149
  dialog = MaskDialog(self)
3136
3150
  dialog.exec()
3137
3151
 
3152
+ def show_crop_dialog(self):
3153
+ """Show the crop dialog"""
3154
+ dialog = CropDialog(self)
3155
+ dialog.exec()
3156
+
3138
3157
  def show_type_dialog(self):
3139
3158
  """Show the type dialog"""
3140
3159
  try:
@@ -3553,6 +3572,103 @@ class ImageViewerWindow(QMainWindow):
3553
3572
  f"Failed to load network: {str(e)}"
3554
3573
  )
3555
3574
 
3575
+ def launch_excelotron(self):
3576
+ """Method to launch Excelotron - call this from a button or menu"""
3577
+ self.excel_manager.launch()
3578
+
3579
+ def close_excelotron(self):
3580
+ """Method to close Excelotron"""
3581
+ self.excel_manager.close()
3582
+
3583
+ def handle_excel_data(self, data_dict, property_name):
3584
+ """Handle data received from Excelotron"""
3585
+ print(f"Received data for property: {property_name}")
3586
+ print(f"Data keys: {list(data_dict.keys())}")
3587
+
3588
+ if property_name == 'Node Centroids':
3589
+
3590
+ try:
3591
+
3592
+ ys = data_dict['Y']
3593
+ xs = data_dict['X']
3594
+ if 'Numerical IDs' in data_dict:
3595
+ nodes = data_dict['Numerical IDs']
3596
+ else:
3597
+ nodes = np.arange(1, len(ys) + 1)
3598
+
3599
+
3600
+ if 'Z' in data_dict:
3601
+ zs = data_dict['Z']
3602
+ else:
3603
+ zs = np.zeros(len(ys))
3604
+
3605
+ centroids = {}
3606
+
3607
+ for i in range(len(nodes)):
3608
+
3609
+ centroids[nodes[i]] = [int(zs[i]), int(ys[i]), int(xs[i])]
3610
+
3611
+ my_network.node_centroids = centroids
3612
+
3613
+ self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
3614
+
3615
+ print("Centroids succesfully set")
3616
+
3617
+ except Exception as e:
3618
+ print(f"Error: {e}")
3619
+
3620
+ elif property_name == 'Node Identities':
3621
+
3622
+ try:
3623
+
3624
+ idens = data_dict['Identity Column']
3625
+
3626
+ if 'Numerical IDs' in data_dict:
3627
+ nodes = data_dict['Numerical IDs']
3628
+ else:
3629
+ nodes = np.arange(1, len(idens) + 1)
3630
+
3631
+ identities = {}
3632
+
3633
+
3634
+ for i in range(len(nodes)):
3635
+
3636
+ identities[nodes[i]] = str(idens[i])
3637
+
3638
+ my_network.node_identities = identities
3639
+
3640
+ self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', title = 'Node Identities')
3641
+
3642
+ print("Identities succesfully set")
3643
+
3644
+ except Exception as e:
3645
+ print(f"Error: {e}")
3646
+
3647
+ elif property_name == 'Node Communities':
3648
+
3649
+ try:
3650
+
3651
+ coms = data_dict['Community Identifier']
3652
+
3653
+ if 'Numerical IDs' in data_dict:
3654
+ nodes = data_dict['Numerical IDs']
3655
+ else:
3656
+ nodes = np.arange(1, len(coms) + 1)
3657
+
3658
+ communities = {}
3659
+
3660
+ for i in range(len(nodes)):
3661
+
3662
+ communities[nodes[i]] = [str(coms[i])]
3663
+
3664
+ my_network.communities = communities
3665
+
3666
+ self.format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
3667
+
3668
+ print("Communities succesfully set")
3669
+
3670
+ except Exception as e:
3671
+ print(f"Error: {e}")
3556
3672
 
3557
3673
 
3558
3674
  def set_active_channel(self, index):
@@ -3783,6 +3899,8 @@ class ImageViewerWindow(QMainWindow):
3783
3899
  except:
3784
3900
  pass
3785
3901
 
3902
+ self.shape = self.channel_data[channel_index].shape
3903
+
3786
3904
  self.update_display(reset_resize = reset_resize)
3787
3905
 
3788
3906
 
@@ -4283,19 +4401,19 @@ class ImageViewerWindow(QMainWindow):
4283
4401
  dialog.exec()
4284
4402
 
4285
4403
  def handle_com_id(self):
4286
- if my_network.node_identities is None:
4287
- print("Node identities must be set")
4288
4404
 
4289
- if my_network.communities is None:
4290
- self.show_partition_dialog()
4405
+ dialog = ComIdDialog(self)
4406
+ dialog.exec()
4291
4407
 
4292
- if my_network.communities is None:
4293
- return
4408
+ def handle_com_neighbor(self):
4294
4409
 
4295
- info = my_network.community_id_info()
4410
+ dialog = ComNeighborDialog(self)
4411
+ dialog.exec()
4296
4412
 
4297
- self.format_for_upperright_table(info, 'Node Identity Type', 'Weighted Proportion in Communities', 'Weighted Average of Community Makeup')
4413
+ def handle_com_cell(self):
4298
4414
 
4415
+ dialog = ComCellDialog(self)
4416
+ dialog.exec()
4299
4417
 
4300
4418
  def show_radial_dialog(self):
4301
4419
  dialog = RadialDialog(self)
@@ -4346,6 +4464,21 @@ class ImageViewerWindow(QMainWindow):
4346
4464
  dialog = CodeDialog(self, sort = sort)
4347
4465
  dialog.exec()
4348
4466
 
4467
+ def closeEvent(self, event):
4468
+ """Override closeEvent to close all windows when main window closes"""
4469
+
4470
+ # Close all Qt windows
4471
+ QApplication.closeAllWindows()
4472
+
4473
+ # Close all matplotlib figures
4474
+ plt.close('all')
4475
+
4476
+ # Accept the close event
4477
+ event.accept()
4478
+
4479
+ # Force quit the application
4480
+ QCoreApplication.quit()
4481
+
4349
4482
 
4350
4483
 
4351
4484
  #TABLE RELATED:
@@ -5542,10 +5675,15 @@ class Show3dDialog(QDialog):
5542
5675
  layout.addRow("Downsample Factor (Optional to speed up display):", self.downsample)
5543
5676
 
5544
5677
  # Network Overlay checkbox (default True)
5545
- self.cubic = QPushButton("cubic")
5678
+ self.cubic = QPushButton("Cubic")
5546
5679
  self.cubic.setCheckable(True)
5547
5680
  self.cubic.setChecked(False)
5548
5681
  layout.addRow("Use cubic downsample (Slower but preserves shape better potentially)?", self.cubic)
5682
+
5683
+ self.box = QPushButton("Box")
5684
+ self.box.setCheckable(True)
5685
+ self.box.setChecked(False)
5686
+ layout.addRow("Include bounding box?", self.box)
5549
5687
 
5550
5688
  # Add Run button
5551
5689
  run_button = QPushButton("Show 3D")
@@ -5564,6 +5702,7 @@ class Show3dDialog(QDialog):
5564
5702
  downsample = None
5565
5703
 
5566
5704
  cubic = self.cubic.isChecked()
5705
+ box = self.box.isChecked()
5567
5706
 
5568
5707
  if cubic:
5569
5708
  order = 3
@@ -5596,7 +5735,7 @@ class Show3dDialog(QDialog):
5596
5735
  arrays_3d.append(self.parent().highlight_overlay)
5597
5736
  colors.append(color_template[4])
5598
5737
 
5599
- n3d.show_3d(arrays_3d, arrays_4d, down_factor = downsample, order = order, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, colors = colors)
5738
+ n3d.show_3d(arrays_3d, arrays_4d, down_factor = downsample, order = order, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, colors = colors, box = box)
5600
5739
 
5601
5740
  self.accept()
5602
5741
 
@@ -5972,7 +6111,7 @@ class PartitionDialog(QDialog):
5972
6111
  dostats = self.stats.isChecked()
5973
6112
 
5974
6113
  try:
5975
- seed = int(self.seed.text()) if self.seed.text() else None
6114
+ seed = int(self.seed.text()) if self.seed.text() else 42
5976
6115
  except:
5977
6116
  seed = None
5978
6117
 
@@ -5993,6 +6132,184 @@ class PartitionDialog(QDialog):
5993
6132
  except Exception as e:
5994
6133
  print(f"Error creating communities: {e}")
5995
6134
 
6135
+ class ComIdDialog(QDialog):
6136
+
6137
+ def __init__(self, parent=None):
6138
+
6139
+ super().__init__(parent)
6140
+ self.setWindowTitle("Select Mode")
6141
+ self.setModal(True)
6142
+
6143
+ layout = QFormLayout(self)
6144
+
6145
+ self.mode = QComboBox()
6146
+ self.mode.addItems(["Average Identities Per Community", "Weighted Average Identity of All Communities", ])
6147
+ self.mode.setCurrentIndex(0)
6148
+ layout.addRow("Mode", self.mode)
6149
+
6150
+ # umap checkbox (default True)
6151
+ self.umap = QPushButton("UMAP")
6152
+ self.umap.setCheckable(True)
6153
+ self.umap.setChecked(True)
6154
+ layout.addRow("Generate UMAP?:", self.umap)
6155
+
6156
+ # weighted checkbox (default True)
6157
+ self.label = QPushButton("Label")
6158
+ self.label.setCheckable(True)
6159
+ self.label.setChecked(False)
6160
+ layout.addRow("If using above - label UMAP points?:", self.label)
6161
+
6162
+
6163
+ # Add Run button
6164
+ run_button = QPushButton("Get Community ID Info")
6165
+ run_button.clicked.connect(self.run)
6166
+ layout.addWidget(run_button)
6167
+
6168
+ def run(self):
6169
+
6170
+ try:
6171
+
6172
+ if my_network.node_identities is None:
6173
+ print("Node identities must be set")
6174
+
6175
+ if my_network.communities is None:
6176
+ self.parent().show_partition_dialog()
6177
+
6178
+ if my_network.communities is None:
6179
+ return
6180
+
6181
+ mode = self.mode.currentIndex()
6182
+
6183
+ umap = self.umap.isChecked()
6184
+ label = self.label.isChecked()
6185
+
6186
+ if mode == 1:
6187
+
6188
+ info = my_network.community_id_info()
6189
+
6190
+ self.parent().format_for_upperright_table(info, 'Node Identity Type', 'Weighted Proportion in Communities', 'Weighted Average of Community Makeup')
6191
+
6192
+ else:
6193
+
6194
+ info, names = my_network.community_id_info_per_com(umap = umap, label = label)
6195
+
6196
+ self.parent().format_for_upperright_table(info, 'Community', names, 'Average of Community Makeup')
6197
+
6198
+ self.accept()
6199
+
6200
+ except Exception as e:
6201
+
6202
+ print(f"Error: {e}")
6203
+
6204
+
6205
+
6206
+ class ComNeighborDialog(QDialog):
6207
+
6208
+ def __init__(self, parent=None):
6209
+
6210
+ super().__init__(parent)
6211
+ self.setWindowTitle("Reassign Communities Based on Identity Similarity?")
6212
+ self.setModal(True)
6213
+
6214
+ layout = QFormLayout(self)
6215
+
6216
+ self.neighborcount = QLineEdit("5")
6217
+ layout.addRow("Num Neighborhoods:", self.neighborcount)
6218
+
6219
+ self.seed = QLineEdit("")
6220
+ layout.addRow("Clustering Seed:", self.seed)
6221
+
6222
+ self.limit = QLineEdit("")
6223
+ layout.addRow("Min Community Size to be grouped (Smaller communities will be placed in neighborhood 0 - does not apply if empty)", self.limit)
6224
+
6225
+ # Add Run button
6226
+ run_button = QPushButton("Get Neighborhoods (Note this overwrites current communities - save your coms first)")
6227
+ run_button.clicked.connect(self.run)
6228
+ layout.addWidget(run_button)
6229
+
6230
+ def run(self):
6231
+
6232
+ try:
6233
+
6234
+ if my_network.node_identities is None:
6235
+ print("Node identities must be set")
6236
+
6237
+ if my_network.communities is None:
6238
+ self.parent().show_partition_dialog()
6239
+
6240
+ if my_network.communities is None:
6241
+ return
6242
+
6243
+ seed = float(self.seed.text()) if self.seed.text().strip() else 42
6244
+
6245
+ limit = int(self.limit.text()) if self.limit.text().strip() else None
6246
+
6247
+
6248
+ neighborcount = int(self.neighborcount.text()) if self.neighborcount.text().strip() else 5
6249
+
6250
+ if self.parent().prev_coms is None:
6251
+
6252
+ self.parent().prev_coms = copy.deepcopy(my_network.communities)
6253
+ my_network.assign_neighborhoods(seed, neighborcount, limit = limit)
6254
+ else:
6255
+ my_network.assign_neighborhoods(seed, neighborcount, limit = limit, prev_coms = self.parent().prev_coms)
6256
+
6257
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'NeighborhoodID', title = 'Neighborhood Partition')
6258
+
6259
+ print("Neighborhoods have been assigned to communities based on similarity")
6260
+
6261
+ self.accept()
6262
+
6263
+ except Exception as e:
6264
+
6265
+ print(f"Error assigning neighborhoods: {e}")
6266
+
6267
+ class ComCellDialog(QDialog):
6268
+
6269
+ def __init__(self, parent=None):
6270
+
6271
+ super().__init__(parent)
6272
+ self.setWindowTitle("Assign Communities Based on Proximity Within Cuboidal Cells?")
6273
+ self.setModal(True)
6274
+
6275
+ layout = QFormLayout(self)
6276
+
6277
+ self.size = QLineEdit("")
6278
+ layout.addRow("Cell Size:", self.size)
6279
+
6280
+ # Add Run button
6281
+ run_button = QPushButton("Get Neighborhoods (Note this overwrites current communities - save your coms first)")
6282
+ run_button.clicked.connect(self.run)
6283
+ layout.addWidget(run_button)
6284
+
6285
+ def run(self):
6286
+
6287
+ try:
6288
+
6289
+ size = int(self.size.text()) if self.size.text().strip() else None
6290
+
6291
+ if size is None:
6292
+ return
6293
+
6294
+ if my_network.node_centroids is None:
6295
+ self.parent().show_centroid_dialog()
6296
+ if my_network.node_centroids is None:
6297
+ return
6298
+
6299
+ my_network.community_cells(size = size)
6300
+
6301
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
6302
+
6303
+ self.accept()
6304
+
6305
+ except Exception as e:
6306
+
6307
+ print(f"Error: {e}")
6308
+
6309
+
6310
+
6311
+
6312
+
5996
6313
 
5997
6314
 
5998
6315
  class RadialDialog(QDialog):
@@ -7316,7 +7633,7 @@ class SLabelDialog(QDialog):
7316
7633
  try:
7317
7634
 
7318
7635
  # Update both the display data and the network object
7319
- binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor)
7636
+ binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
7320
7637
 
7321
7638
  label_array = sdl.invert_array(label_array)
7322
7639
 
@@ -7433,12 +7750,101 @@ class ThresholdDialog(QDialog):
7433
7750
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
7434
7751
  return msg.exec() == QMessageBox.StandardButton.Yes
7435
7752
 
7753
+ class ExcelotronManager(QObject):
7754
+ # Signal to emit when data is received from Excelotron
7755
+ data_received = pyqtSignal(dict, str) # dictionary, property_name
7756
+
7757
+ def __init__(self, parent=None):
7758
+ super().__init__(parent)
7759
+ self.excelotron_window = None
7760
+ self.last_data = None
7761
+ self.last_property = None
7762
+
7763
+ def launch(self):
7764
+ """Launch the Excelotron window"""
7765
+
7766
+ if self.excelotron_window is None:
7767
+ ExcelGUIClass = excelotron.main(standalone=False)
7768
+ self.excelotron_window = ExcelGUIClass()
7769
+ self.excelotron_window.data_exported.connect(self._on_data_exported)
7770
+ # Connect to both close event and destroyed signal
7771
+ self.excelotron_window.destroyed.connect(self._on_window_destroyed)
7772
+ self.excelotron_window.closeEvent = self._create_close_handler(self.excelotron_window.closeEvent)
7773
+ self.excelotron_window.show()
7774
+ else:
7775
+ self.excelotron_window.raise_()
7776
+ self.excelotron_window.activateWindow()
7777
+
7778
+ def _create_close_handler(self, original_close_event):
7779
+ """Create a close event handler that cleans up properly"""
7780
+ def close_handler(event):
7781
+ self._cleanup_window()
7782
+ original_close_event(event)
7783
+ return close_handler
7784
+
7785
+ def close(self):
7786
+ """Close the Excelotron window"""
7787
+ if self.excelotron_window is not None:
7788
+ self.excelotron_window.close()
7789
+ self._cleanup_window()
7790
+
7791
+ def _cleanup_window(self):
7792
+ """Properly cleanup the window reference"""
7793
+ if self.excelotron_window is not None:
7794
+ try:
7795
+ # Disconnect all signals to prevent issues
7796
+ self.excelotron_window.data_exported.disconnect()
7797
+ self.excelotron_window.destroyed.disconnect()
7798
+ except:
7799
+ pass # Ignore if already disconnected
7800
+
7801
+ # Schedule for deletion
7802
+ self.excelotron_window.deleteLater()
7803
+ self.excelotron_window = None
7804
+
7805
+ def is_open(self):
7806
+ """Check if Excelotron window is open"""
7807
+ is_open = self.excelotron_window is not None
7808
+ return is_open
7809
+
7810
+ def _on_data_exported(self, data_dict, property_name):
7811
+ """Internal slot to handle data from Excelotron"""
7812
+ self.last_data = data_dict
7813
+ self.last_property = property_name
7814
+ # Re-emit the signal for parent to handle
7815
+ self.data_received.emit(data_dict, property_name)
7816
+
7817
+ def _on_window_destroyed(self):
7818
+ """Handle when the Excelotron window is destroyed/closed"""
7819
+ self.excelotron_window = None
7820
+
7821
+ def get_last_data(self):
7822
+ """Get the last exported data"""
7823
+ return self.last_data, self.last_property
7436
7824
 
7437
7825
  class MachineWindow(QMainWindow):
7438
7826
 
7439
7827
  def __init__(self, parent=None, GPU = False):
7440
7828
  super().__init__(parent)
7441
7829
 
7830
+ if self.parent().active_channel == 0:
7831
+ if self.parent().channel_data[0] is not None:
7832
+ try:
7833
+ active_data = self.parent().channel_data[0]
7834
+ act_channel = 0
7835
+ except:
7836
+ active_data = self.parent().channel_data[1]
7837
+ act_channel = 1
7838
+ else:
7839
+ active_data = self.parent().channel_data[1]
7840
+ act_channel = 1
7841
+
7842
+ try:
7843
+ array1 = np.zeros_like(active_data).astype(np.uint8)
7844
+ except:
7845
+ print("No data in nodes channel")
7846
+ return
7847
+
7442
7848
  self.setWindowTitle("Threshold")
7443
7849
 
7444
7850
  # Create central widget and layout
@@ -7460,25 +7866,6 @@ class MachineWindow(QMainWindow):
7460
7866
 
7461
7867
  self.parent().pen_button.setEnabled(False)
7462
7868
 
7463
-
7464
- if self.parent().active_channel == 0:
7465
- if self.parent().channel_data[0] is not None:
7466
- try:
7467
- active_data = self.parent().channel_data[0]
7468
- act_channel = 0
7469
- except:
7470
- active_data = self.parent().channel_data[1]
7471
- act_channel = 1
7472
- else:
7473
- active_data = self.parent().channel_data[1]
7474
- act_channel = 1
7475
-
7476
- try:
7477
- array1 = np.zeros_like(active_data).astype(np.uint8)
7478
- except:
7479
- print("No data in nodes channel")
7480
- return
7481
-
7482
7869
  array3 = np.zeros_like(active_data).astype(np.uint8)
7483
7870
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
7484
7871
 
@@ -8863,6 +9250,91 @@ class MaskDialog(QDialog):
8863
9250
  except Exception as e:
8864
9251
  print(f"Error masking: {e}")
8865
9252
 
9253
+ class CropDialog(QDialog):
9254
+
9255
+ def __init__(self, parent=None):
9256
+
9257
+ try:
9258
+
9259
+ super().__init__(parent)
9260
+ self.setWindowTitle("Crop Image?")
9261
+ self.setModal(True)
9262
+
9263
+ layout = QFormLayout(self)
9264
+
9265
+ self.xmin = QLineEdit("0")
9266
+ layout.addRow("X Min", self.xmin)
9267
+
9268
+ self.xmax = QLineEdit(f"{self.parent().shape[2]}")
9269
+ layout.addRow("X Max", self.xmax)
9270
+
9271
+ self.ymin = QLineEdit("0")
9272
+ layout.addRow("Y Min", self.ymin)
9273
+
9274
+ self.ymax = QLineEdit(f"{self.parent().shape[1]}")
9275
+ layout.addRow("Y Max", self.ymax)
9276
+
9277
+ self.zmin = QLineEdit("0")
9278
+ layout.addRow("Z Min", self.zmin)
9279
+
9280
+ self.zmax = QLineEdit(f"{self.parent().shape[0]}")
9281
+ layout.addRow("Z Max", self.zmax)
9282
+
9283
+ # Add Run button
9284
+ run_button = QPushButton("Run")
9285
+ run_button.clicked.connect(self.run)
9286
+ layout.addRow(run_button)
9287
+
9288
+ except:
9289
+ pass
9290
+
9291
+ def run(self):
9292
+
9293
+ try:
9294
+
9295
+ xmin = int(self.xmin.text()) if self.xmin.text() else 0
9296
+ ymin = int(self.ymin.text()) if self.ymin.text() else 0
9297
+ zmin = int(self.zmin.text()) if self.zmin.text() else 0
9298
+ xmax = int(self.xmax.text()) if self.xmax.text() else self.parent().shape[2]
9299
+ ymax = int(self.ymax.text()) if self.xmax.text() else self.parent().shape[1]
9300
+ zmax = int(self.zmax.text()) if self.xmax.text() else self.parent().shape[0]
9301
+
9302
+ args = xmin, ymin, zmin, xmax, ymax, zmax
9303
+
9304
+ for i, array in enumerate(self.parent().channel_data):
9305
+
9306
+ if array is None:
9307
+
9308
+ continue
9309
+
9310
+ else:
9311
+
9312
+ array = self.reslice_3d_array(array, args)
9313
+
9314
+ self.parent().load_channel(i, array, data = True)
9315
+
9316
+ self.accept()
9317
+
9318
+ except Exception as e:
9319
+
9320
+ print(f"Error cropping: {e}")
9321
+
9322
+
9323
+
9324
+
9325
+
9326
+
9327
+
9328
+
9329
+ def reslice_3d_array(self, array, args):
9330
+ """Internal method used for the secondary algorithm to reslice subarrays around nodes."""
9331
+
9332
+ x_start, y_start, z_start, x_end, y_end, z_end = args
9333
+
9334
+ # Reslice the array
9335
+ array = array[z_start:z_end+1, y_start:y_end+1, x_start:x_end+1]
9336
+
9337
+ return array
8866
9338
 
8867
9339
 
8868
9340
  class TypeDialog(QDialog):
@@ -9226,6 +9698,12 @@ class CentroidNodeDialog(QDialog):
9226
9698
 
9227
9699
  layout = QFormLayout(self)
9228
9700
 
9701
+ # Add mode selection dropdown
9702
+ self.mode_selector = QComboBox()
9703
+ self.mode_selector.addItems(["Starting at 0", "Starting at Min Centroids (will transpose centroids)"])
9704
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
9705
+ layout.addRow("Execution Mode:", self.mode_selector)
9706
+
9229
9707
  # Add Run button
9230
9708
  run_button = QPushButton("Run Node Generation? (Will override current nodes). Note it is presumed your nodes begin at 1, not 0.")
9231
9709
  run_button.clicked.connect(self.run_nodes)
@@ -9255,7 +9733,18 @@ class CentroidNodeDialog(QDialog):
9255
9733
  )
9256
9734
  return
9257
9735
 
9258
- my_network.nodes = my_network.centroid_array()
9736
+ mode = self.mode_selector.currentIndex()
9737
+
9738
+ if mode == 0:
9739
+
9740
+ my_network.nodes = my_network.centroid_array()
9741
+
9742
+ else:
9743
+
9744
+ my_network.nodes, my_network.centroids = my_network.centroid_array(clip = True)
9745
+
9746
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
9747
+
9259
9748
 
9260
9749
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
9261
9750
 
@@ -9338,7 +9827,13 @@ class GenNodesDialog(QDialog):
9338
9827
  # Auto checkbox
9339
9828
  self.auto = QPushButton("Auto")
9340
9829
  self.auto.setCheckable(True)
9341
- self.auto.setChecked(True)
9830
+ try:
9831
+ if my_network.edges.shape[0] == 1:
9832
+ self.auto.setChecked(False)
9833
+ else:
9834
+ self.auto.setChecked(True)
9835
+ except:
9836
+ self.auto.setChecked(True)
9342
9837
  rec_layout.addWidget(QLabel("Attempt to Auto Correct Skeleton Looping:"), 1, 0)
9343
9838
  rec_layout.addWidget(self.auto, 1, 1)
9344
9839
 
@@ -9503,10 +9998,10 @@ class BranchDialog(QDialog):
9503
9998
  correction_layout = QGridLayout()
9504
9999
 
9505
10000
  # Branch Fix checkbox
9506
- self.fix = QPushButton("Auto-Correct Branches")
10001
+ self.fix = QPushButton("Auto-Correct 1")
9507
10002
  self.fix.setCheckable(True)
9508
10003
  self.fix.setChecked(False)
9509
- correction_layout.addWidget(QLabel("Attempt to auto-correct branch labels:"), 0, 0)
10004
+ correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
9510
10005
  correction_layout.addWidget(self.fix, 0, 1)
9511
10006
 
9512
10007
  # Fix value
@@ -9518,6 +10013,12 @@ class BranchDialog(QDialog):
9518
10013
  self.seed = QLineEdit('')
9519
10014
  correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
9520
10015
  correction_layout.addWidget(self.seed, 2, 1)
10016
+
10017
+ self.fix2 = QPushButton("Auto-Correct 2")
10018
+ self.fix2.setCheckable(True)
10019
+ self.fix2.setChecked(True)
10020
+ correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Internal Labels: "), 3, 0)
10021
+ correction_layout.addWidget(self.fix2, 3, 1)
9521
10022
 
9522
10023
  correction_group.setLayout(correction_layout)
9523
10024
  main_layout.addWidget(correction_group)
@@ -9587,6 +10088,7 @@ class BranchDialog(QDialog):
9587
10088
  GPU = self.GPU.isChecked()
9588
10089
  cubic = self.cubic.isChecked()
9589
10090
  fix = self.fix.isChecked()
10091
+ fix2 = self.fix2.isChecked()
9590
10092
  fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
9591
10093
  seed = int(self.seed.text()) if self.seed.text() else None
9592
10094
 
@@ -9603,6 +10105,25 @@ class BranchDialog(QDialog):
9603
10105
 
9604
10106
  output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
9605
10107
 
10108
+ if fix2:
10109
+
10110
+ temp_network = n3d.Network_3D(nodes = output)
10111
+
10112
+ max_val = np.max(temp_network.nodes)
10113
+
10114
+ background = temp_network.nodes == 0
10115
+
10116
+ background = background * max_val
10117
+
10118
+ temp_network.nodes = temp_network.nodes + background
10119
+
10120
+ del background
10121
+
10122
+ temp_network.morph_proximity(search = [3,3], fastdil = True) #Detect network of nearby branches
10123
+
10124
+ output = n3d.fix_branches(output, temp_network.network, max_val)
10125
+
10126
+
9606
10127
  if fix:
9607
10128
 
9608
10129
  temp_network = n3d.Network_3D(nodes = output)
@@ -9611,7 +10132,7 @@ class BranchDialog(QDialog):
9611
10132
 
9612
10133
  temp_network.community_partition(weighted = False, style = 1, dostats = False, seed = seed) #Find communities with louvain, unweighted params
9613
10134
 
9614
- targs = n3d.fix_branches(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
10135
+ targs = n3d.fix_branches_network(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
9615
10136
 
9616
10137
  temp_network.com_to_node(targs)
9617
10138
 
@@ -9798,6 +10319,11 @@ class ModifyDialog(QDialog):
9798
10319
  self.revid.setCheckable(True)
9799
10320
  self.revid.setChecked(False)
9800
10321
  layout.addRow("Remove Unassigned IDs from Centroid List?:", self.revid)
10322
+
10323
+ self.remove = QPushButton("Remove Missing")
10324
+ self.remove.setCheckable(True)
10325
+ self.remove.setChecked(False)
10326
+ layout.addRow("Remove Any Nodes Not in Nodes Channel From Properties?:", self.remove)
9801
10327
 
9802
10328
  # trunk checkbox (default false)
9803
10329
  self.trunk = QPushButton("Remove Trunk")
@@ -9876,6 +10402,7 @@ class ModifyDialog(QDialog):
9876
10402
  prune = self.prune.isChecked()
9877
10403
  isolate = self.isolate.isChecked()
9878
10404
  comcollapse = self.comcollapse.isChecked()
10405
+ remove = self.remove.isChecked()
9879
10406
 
9880
10407
 
9881
10408
  if isolate and my_network.node_identities is not None:
@@ -9889,6 +10416,21 @@ class ModifyDialog(QDialog):
9889
10416
  pass
9890
10417
 
9891
10418
 
10419
+ if remove:
10420
+ my_network.purge_properties()
10421
+ try:
10422
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
10423
+ except:
10424
+ pass
10425
+ try:
10426
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
10427
+ except:
10428
+ pass
10429
+ try:
10430
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
10431
+ except:
10432
+ pass
10433
+
9892
10434
 
9893
10435
  if edgeweight:
9894
10436
  my_network.remove_edge_weights()
@@ -10483,14 +11025,9 @@ class ProxDialog(QDialog):
10483
11025
  my_network.id_overlay = my_network.draw_node_indices(directory=directory)
10484
11026
 
10485
11027
  # Update channel data
10486
- self.parent().channel_data[2] = my_network.network_overlay
10487
- self.parent().channel_data[3] = my_network.id_overlay
11028
+ self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True)
11029
+ self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
10488
11030
 
10489
- # Enable the overlay channel buttons
10490
- self.parent().channel_buttons[2].setEnabled(True)
10491
- self.parent().channel_buttons[3].setEnabled(True)
10492
-
10493
-
10494
11031
  self.parent().update_display()
10495
11032
  self.accept()
10496
11033