nettracer3d 0.6.0__py3-none-any.whl → 0.6.2__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.
nettracer3d/nettracer.py CHANGED
@@ -27,7 +27,6 @@ from skimage import morphology as mpg
27
27
  from . import smart_dilate
28
28
  from . import modularity
29
29
  from . import simple_network
30
- from . import hub_getter
31
30
  from . import community_extractor
32
31
  from . import network_analysis
33
32
  from . import morphology
@@ -3934,28 +3933,6 @@ class Network_3D:
3934
3933
 
3935
3934
  return degrees, nodes
3936
3935
 
3937
- def get_hubs(self, proportion = None, down_factor = 1, directory = None):
3938
- """
3939
- Method to isolate hub regions of a network (Removing all nodes below some proportion of highest degrees), also generating overlays that relate this information to the 3D structure.
3940
- Overlays include a grayscale image where nodes are assigned a grayscale value corresponding to their degree, and a numerical index where numbers are drawn at nodes corresponding to their degree.
3941
- These will be saved to the active directory if none is specified. Note calculations will be done with node_centroids unless a down_factor is passed. Note that a down_factor must be passed if there are no node_centroids.
3942
- :param proportion: (Optional - Val = None; Float). A float of 0 to 1 that details what proportion of highest node degrees to include in the output. Note that this value will be set to 0.1 by default.
3943
- :param down_factor: (Optional - Val = 1; int). A factor to downsample nodes by while calculating centroids, assuming no node_centroids property was set.
3944
- :param directory: (Optional - Val = None; string). A path to a directory to save outputs.
3945
- :returns: A dictionary of degree values for each node above the desired proportion of highest degree nodes.
3946
- """
3947
- if down_factor > 1:
3948
- centroids = self._node_centroids.copy()
3949
- for item in self._node_centroids:
3950
- centroids[item] = np.round((self._node_centroids[item]) / down_factor)
3951
- nodes = downsample(self._nodes, down_factor)
3952
- hubs = hub_getter.get_hubs(nodes, self._network, proportion, directory = directory, centroids = centroids)
3953
-
3954
- else:
3955
- hubs = hub_getter.get_hubs(self._nodes, self._network, proportion, directory = directory, centroids = self._node_centroids)
3956
-
3957
- return hubs
3958
-
3959
3936
 
3960
3937
  def isolate_connected_component(self, key = None, directory = None, full_edges = None, gen_images = True):
3961
3938
  """
@@ -1259,19 +1259,28 @@ class ImageViewerWindow(QMainWindow):
1259
1259
  nodes = list(np.unique(my_network.nodes))
1260
1260
  if nodes[0] == 0:
1261
1261
  del nodes[0]
1262
+ num = (self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2])
1263
+ print(f"Found {len(nodes)} node objects")
1262
1264
  else:
1263
1265
  nodes = []
1264
1266
  if edges:
1265
1267
  edges = list(np.unique(my_network.edges))
1268
+ num = (self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2])
1266
1269
  if edges[0] == 0:
1267
1270
  del edges[0]
1271
+ print(f"Found {len(edges)} edge objects")
1268
1272
  else:
1269
1273
  edges = []
1270
1274
 
1271
- self.clicked_values['nodes'] += nodes
1272
- self.clicked_values['edges'] += edges
1275
+ self.clicked_values['nodes'] = nodes
1276
+ self.clicked_values['edges'] = edges
1273
1277
 
1274
- self.create_highlight_overlay(edge_indices = self.clicked_values['edges'], node_indices = self.clicked_values['nodes'])
1278
+
1279
+ if num > self.mini_thresh:
1280
+ self.mini_overlay = True
1281
+ self.create_mini_overlay(node_indices = nodes, edge_indices = edges)
1282
+ else:
1283
+ self.create_highlight_overlay(edge_indices = self.clicked_values['edges'], node_indices = self.clicked_values['nodes'])
1275
1284
 
1276
1285
  except Exception as e:
1277
1286
  print(f"Error: {e}")
@@ -2466,6 +2475,8 @@ class ImageViewerWindow(QMainWindow):
2466
2475
  searchoverlay_action.triggered.connect(self.show_search_dialog)
2467
2476
  shuffle_action = overlay_menu.addAction("Shuffle")
2468
2477
  shuffle_action.triggered.connect(self.show_shuffle_dialog)
2478
+ arbitrary_action = image_menu.addAction("Select Objects")
2479
+ arbitrary_action.triggered.connect(self.show_arbitrary_dialog)
2469
2480
  show3d_action = image_menu.addAction("Show 3D (Napari)")
2470
2481
  show3d_action.triggered.connect(self.show3d_dialog)
2471
2482
 
@@ -2596,6 +2607,11 @@ class ImageViewerWindow(QMainWindow):
2596
2607
  dialog = WatershedDialog(self)
2597
2608
  dialog.exec()
2598
2609
 
2610
+ def show_arbitrary_dialog(self):
2611
+ """Show the arbitrary selection dialog."""
2612
+ dialog = ArbitraryDialog(self)
2613
+ dialog.exec()
2614
+
2599
2615
  def show_invert_dialog(self):
2600
2616
  """Show the watershed parameter dialog."""
2601
2617
  dialog = InvertDialog(self)
@@ -3504,8 +3520,8 @@ class ImageViewerWindow(QMainWindow):
3504
3520
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3505
3521
  # Convert slider values (0-100) to data values (0-1)
3506
3522
  min_val, max_val = values
3507
- self.channel_brightness[channel_index]['min'] = min_val / 255 #Accomodate 32 bit data?
3508
- self.channel_brightness[channel_index]['max'] = max_val / 255
3523
+ self.channel_brightness[channel_index]['min'] = min_val / 65535 #Accomodate 32 bit data?
3524
+ self.channel_brightness[channel_index]['max'] = max_val / 65535
3509
3525
  self.update_display(preserve_zoom = (current_xlim, current_ylim))
3510
3526
 
3511
3527
 
@@ -4636,14 +4652,14 @@ class BrightnessContrastDialog(QDialog):
4636
4652
  # Create range slider
4637
4653
  slider = QRangeSlider(Qt.Orientation.Horizontal)
4638
4654
  slider.setMinimum(0)
4639
- slider.setMaximum(255)
4640
- slider.setValue((0, 255))
4655
+ slider.setMaximum(65535)
4656
+ slider.setValue((0, 65535))
4641
4657
  self.brightness_sliders.append(slider)
4642
4658
 
4643
4659
  # Create max value input
4644
4660
  max_input = QLineEdit()
4645
4661
  max_input.setFixedWidth(50)
4646
- max_input.setText("255")
4662
+ max_input.setText("65535")
4647
4663
  self.max_inputs.append(max_input)
4648
4664
 
4649
4665
  # Add all components to slider container
@@ -4692,8 +4708,8 @@ class BrightnessContrastDialog(QDialog):
4692
4708
  max_val = self.parse_input_value(self.max_inputs[channel].text())
4693
4709
  current_min, current_max = self.brightness_sliders[channel].value()
4694
4710
 
4695
- if max_val > 255:
4696
- max_val = 255
4711
+ if max_val > 65535:
4712
+ max_val = 65535
4697
4713
  # Ensure max doesn't go below min
4698
4714
  max_val = max(max_val, current_min + 1)
4699
4715
 
@@ -4713,8 +4729,8 @@ class BrightnessContrastDialog(QDialog):
4713
4729
  value = float(text)
4714
4730
  # Round to nearest integer
4715
4731
  value = int(round(value))
4716
- # Clamp between 0 and 255
4717
- return max(0, min(255, value))
4732
+ # Clamp between 0 and 65535
4733
+ return max(0, min(65535, value))
4718
4734
  except ValueError:
4719
4735
  raise ValueError("Invalid input")
4720
4736
 
@@ -4761,7 +4777,168 @@ class ColorDialog(QDialog):
4761
4777
 
4762
4778
  # Update the display
4763
4779
  self.parent().update_display()
4764
- self.accept()
4780
+ self.accept()
4781
+
4782
+ class ArbitraryDialog(QDialog):
4783
+ def __init__(self, parent=None):
4784
+ super().__init__(parent)
4785
+ self.setWindowTitle("Arbitrary Selector")
4786
+ self.setModal(True)
4787
+
4788
+ # Main layout
4789
+ main_layout = QVBoxLayout(self)
4790
+
4791
+ # Form layout for inputs
4792
+ layout = QFormLayout()
4793
+ main_layout.addLayout(layout)
4794
+
4795
+ self.mode_selector = QComboBox()
4796
+ self.mode_selector.addItems(["nodes", "edges"])
4797
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
4798
+ layout.addRow("Type to select:", self.mode_selector)
4799
+
4800
+ # Selection section
4801
+ excel_button = QPushButton("Import selection from spreadsheet (Col 1)")
4802
+ excel_button.clicked.connect(self.import_excel)
4803
+ layout.addWidget(excel_button)
4804
+
4805
+ self.select = QLineEdit("")
4806
+ layout.addRow("Select the following? (Use this format - '1,2,3,4' etc:", self.select)
4807
+
4808
+ # Deselection section
4809
+ deexcel_button = QPushButton("Import deselection from spreadsheet (Col 1)")
4810
+ deexcel_button.clicked.connect(self.import_deexcel)
4811
+ layout.addWidget(deexcel_button)
4812
+
4813
+ self.deselect = QLineEdit("")
4814
+ layout.addRow("Deselect the following? (Use this format - '1,2,3,4' etc:", self.deselect)
4815
+
4816
+ # Run button
4817
+ run_button = QPushButton("Run")
4818
+ run_button.clicked.connect(self.process_selections)
4819
+ main_layout.addWidget(run_button)
4820
+
4821
+ def import_excel(self):
4822
+ """Import selection from Excel/CSV and populate the select QLineEdit."""
4823
+ file_path, _ = QFileDialog.getOpenFileName(
4824
+ self, "Select File", "", "Spreadsheet Files (*.xlsx *.xls *.csv)"
4825
+ )
4826
+
4827
+ if file_path:
4828
+ try:
4829
+ selection_list = self.read_selection_from_file(file_path)
4830
+ selection_string = ",".join(map(str, selection_list))
4831
+ self.select.setText(selection_string)
4832
+ except Exception as e:
4833
+ QMessageBox.critical(self, "Error", f"Failed to import: {str(e)}")
4834
+
4835
+ def import_deexcel(self):
4836
+ """Import deselection from Excel/CSV and populate the deselect QLineEdit."""
4837
+ file_path, _ = QFileDialog.getOpenFileName(
4838
+ self, "Select File", "", "Spreadsheet Files (*.xlsx *.xls *.csv)"
4839
+ )
4840
+
4841
+ if file_path:
4842
+ try:
4843
+ deselection_list = self.read_selection_from_file(file_path)
4844
+ deselection_string = ",".join(map(str, deselection_list))
4845
+ self.deselect.setText(deselection_string)
4846
+ except Exception as e:
4847
+ QMessageBox.critical(self, "Error", f"Failed to import: {str(e)}")
4848
+
4849
+ def read_selection_from_file(self, file_path):
4850
+ """Read selection IDs from Excel/CSV file and return as a list."""
4851
+ # Determine file type and read accordingly
4852
+ if file_path.lower().endswith('.csv'):
4853
+ # Read CSV file
4854
+ df = pd.read_csv(file_path, header=None)
4855
+ else:
4856
+ # Read Excel file
4857
+ df = pd.read_excel(file_path, header=None)
4858
+
4859
+ # Check if first row looks like a header
4860
+ first_row = df.iloc[0]
4861
+ if all(isinstance(x, str) for x in first_row):
4862
+ # First row is likely a header, skip it
4863
+ values = df.iloc[1:, 0].dropna().tolist()
4864
+ else:
4865
+ # No header, use all rows
4866
+ values = df.iloc[:, 0].dropna().tolist()
4867
+
4868
+ # Convert to integers when possible, keep floats when necessary
4869
+ processed_values = []
4870
+ for val in values:
4871
+ try:
4872
+ # Try to convert to int first
4873
+ processed_values.append(int(val))
4874
+ except ValueError:
4875
+ try:
4876
+ # If int fails, try float
4877
+ processed_values.append(float(val))
4878
+ except ValueError:
4879
+ # Skip values that can't be converted to numbers
4880
+ continue
4881
+
4882
+ return processed_values
4883
+
4884
+ def process_selections(self):
4885
+ """Process the selection and deselection inputs."""
4886
+ try:
4887
+ from ast import literal_eval
4888
+ # Get values from QLineEdit fields
4889
+ select_text = self.select.text()
4890
+ deselect_text = self.deselect.text()
4891
+
4892
+ # Format text for literal_eval by adding brackets
4893
+ if select_text:
4894
+ select_list = literal_eval(f"[{select_text}]")
4895
+ else:
4896
+ select_list = []
4897
+
4898
+ if deselect_text:
4899
+ deselect_list = literal_eval(f"[{deselect_text}]")
4900
+ else:
4901
+ deselect_list = []
4902
+
4903
+ # Get the current mode
4904
+ mode = self.mode_selector.currentText()
4905
+
4906
+ if mode == 'nodes':
4907
+ num = self.parent().channel_data[0].shape[0] * self.parent().channel_data[0].shape[1] * self.parent().channel_data[0].shape[2]
4908
+ else:
4909
+ num = self.parent().channel_data[1].shape[0] * self.parent().channel_data[1].shape[1] * self.parent().channel_data[1].shape[2]
4910
+
4911
+
4912
+ for item in deselect_list:
4913
+ try:
4914
+ self.parent().clicked_values[mode].remove(item)
4915
+ except:
4916
+ pass #Forgive mistakes
4917
+
4918
+ for item in select_list:
4919
+ try:
4920
+ self.parent().clicked_values[mode].append(item)
4921
+ except:
4922
+ pass
4923
+
4924
+ self.parent().clicked_values[mode] = list(set(self.parent().clicked_values[mode]))
4925
+
4926
+ if num > self.parent().mini_thresh:
4927
+ self.parent().mini_overlay = True
4928
+ self.parent().create_mini_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
4929
+ else:
4930
+ self.parent().create_highlight_overlay(
4931
+ node_indices=self.parent().clicked_values['nodes'],
4932
+ edge_indices=self.parent().clicked_values['edges']
4933
+ )
4934
+
4935
+
4936
+
4937
+ # Close the dialog after processing
4938
+ self.accept()
4939
+
4940
+ except Exception as e:
4941
+ QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
4765
4942
 
4766
4943
  class Show3dDialog(QDialog):
4767
4944
  def __init__(self, parent=None):
@@ -6262,7 +6439,7 @@ class MachineWindow(QMainWindow):
6262
6439
  # Group 2: Processing Options (GPU)
6263
6440
  processing_group = QGroupBox("Processing Options")
6264
6441
  processing_layout = QHBoxLayout()
6265
- self.GPU = QPushButton("GPU")
6442
+ self.GPU = QPushButton("GPU (Beta)")
6266
6443
  self.GPU.setCheckable(True)
6267
6444
  self.GPU.setChecked(False)
6268
6445
  self.GPU.clicked.connect(self.toggle_GPU)
@@ -6298,9 +6475,15 @@ class MachineWindow(QMainWindow):
6298
6475
  seg_button = QPushButton("Preview Segment")
6299
6476
  self.seg_button = seg_button
6300
6477
  seg_button.clicked.connect(self.start_segmentation)
6478
+ self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM. Recommended unless you have a lot)")
6479
+ self.lock_button.setCheckable(True)
6480
+ self.lock_button.setChecked(True)
6481
+ self.lock_button.clicked.connect(self.toggle_lock)
6482
+ self.mem_lock = True
6301
6483
  full_button = QPushButton("Segment All")
6302
6484
  full_button.clicked.connect(self.segment)
6303
6485
  segmentation_layout.addWidget(seg_button)
6486
+ segmentation_layout.addWidget(self.lock_button)
6304
6487
  segmentation_layout.addWidget(full_button)
6305
6488
  segmentation_group.setLayout(segmentation_layout)
6306
6489
 
@@ -6320,6 +6503,9 @@ class MachineWindow(QMainWindow):
6320
6503
  self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=True)
6321
6504
  self.segmentation_worker = None
6322
6505
 
6506
+ def toggle_lock(self):
6507
+
6508
+ self.mem_lock = self.lock_button.isChecked()
6323
6509
 
6324
6510
 
6325
6511
  def toggle_GPU(self):
@@ -6399,13 +6585,16 @@ class MachineWindow(QMainWindow):
6399
6585
  if not self.use_two:
6400
6586
  self.previewing = False
6401
6587
  try:
6402
- self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu, use_two = self.use_two)
6403
- self.trained = True
6588
+ try:
6589
+ self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu, use_two = self.use_two, mem_lock = self.mem_lock)
6590
+ self.trained = True
6591
+ except Exception as e:
6592
+ print("Error training. Perhaps you forgot both foreground and background markers? I need both!")
6404
6593
  except MemoryError:
6405
6594
  QMessageBox.critical(
6406
6595
  self,
6407
6596
  "Alert",
6408
- "Out of memory computing feature maps. Note these for 3D require 7x the RAM of the active image (or 9x for the detailed map).\n Please use 2D slice models if you do not have enough RAM."
6597
+ "Out of memory computing feature maps. Note these for 3D require 7x the RAM of the active image (or 9x for the detailed map).\n Please use 2D slice models or RAM lock if you do not have enough RAM."
6409
6598
  )
6410
6599
 
6411
6600
 
@@ -6432,7 +6621,7 @@ class MachineWindow(QMainWindow):
6432
6621
  if not self.trained:
6433
6622
  return
6434
6623
  else:
6435
- self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self)
6624
+ self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
6436
6625
  self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
6437
6626
  self.segmentation_worker.finished.connect(self.segmentation_finished)
6438
6627
  current_xlim = self.parent().ax.get_xlim()
@@ -6669,7 +6858,7 @@ class SegmentationWorker(QThread):
6669
6858
  finished = pyqtSignal()
6670
6859
  chunk_processed = pyqtSignal()
6671
6860
 
6672
- def __init__(self, highlight_overlay, segmenter, use_gpu, use_two, previewing, machine_window):
6861
+ def __init__(self, highlight_overlay, segmenter, use_gpu, use_two, previewing, machine_window, mem_lock):
6673
6862
  super().__init__()
6674
6863
  self.overlay = highlight_overlay
6675
6864
  self.segmenter = segmenter
@@ -6677,6 +6866,7 @@ class SegmentationWorker(QThread):
6677
6866
  self.use_two = use_two
6678
6867
  self.previewing = previewing
6679
6868
  self.machine_window = machine_window
6869
+ self.mem_lock = mem_lock
6680
6870
  self._stop = False
6681
6871
  self.update_interval = 1 # Increased to 500ms
6682
6872
  self.chunks_since_update = 0
@@ -7612,9 +7802,9 @@ class InvertDialog(QDialog):
7612
7802
  if active_data.dtype == 'uint8' or 'int8':
7613
7803
  num = 255
7614
7804
  elif active_data.dtype == 'uint16' or 'int16':
7615
- num = 65,535
7805
+ num = 65535
7616
7806
  elif active_data.dtype == 'uint32' or 'int32':
7617
- num = 2,147,483,647
7807
+ num = 2147483647
7618
7808
 
7619
7809
  result = (num - active_data
7620
7810
  )