nettracer3d 0.5.4__tar.gz → 0.5.5__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.5.4/src/nettracer3d.egg-info → nettracer3d-0.5.5}/PKG-INFO +5 -5
  2. nettracer3d-0.5.5/README.md +17 -0
  3. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/pyproject.toml +1 -1
  4. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/nettracer_gui.py +248 -43
  5. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/segmenter.py +576 -79
  6. {nettracer3d-0.5.4 → nettracer3d-0.5.5/src/nettracer3d.egg-info}/PKG-INFO +5 -5
  7. nettracer3d-0.5.4/README.md +0 -17
  8. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/LICENSE +0 -0
  9. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/setup.cfg +0 -0
  10. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/__init__.py +0 -0
  11. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/community_extractor.py +0 -0
  12. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/hub_getter.py +0 -0
  13. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/modularity.py +0 -0
  14. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/morphology.py +0 -0
  15. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/nettracer.py +0 -0
  16. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/network_analysis.py +0 -0
  17. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/network_draw.py +0 -0
  18. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/node_draw.py +0 -0
  19. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/proximity.py +0 -0
  20. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/run.py +0 -0
  21. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/simple_network.py +0 -0
  22. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/smart_dilate.py +0 -0
  23. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  24. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  25. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  26. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d.egg-info/requires.txt +0 -0
  27. {nettracer3d-0.5.4 → nettracer3d-0.5.5}/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.5.4
3
+ Version: 0.5.5
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <mclaughlinliam99@gmail.com>
6
6
  Project-URL: User_Tutorial, https://www.youtube.com/watch?v=cRatn5VTWDY
@@ -44,10 +44,10 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
44
44
 
45
45
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
46
46
 
47
- -- Version 0.5.4 updates --
47
+ -- Version 0.5.5 updates --
48
48
 
49
- 1. Added new function to GUI in image -> overlays -> color nodes/edges. Generates a rgb array corresponding to the nodes/edge labels where each node/edge (depending which array is selected) is randomly assigned a unique rgb color in an overlay channel. This can be used, for example, to color code labeled branches for easy identification of which branch is which.
49
+ 1. When the highlight overlay is being used in big overlays (and therefore defaulting to the mini overlay), updated system to just make one 2d overlay that regenerates on slice movement. Decided this was better than having it sit in a bigger 3d array when said array wasn't being used for anything and had to be rebuilt on slice movement or 3d operations anyway.
50
50
 
51
- 2. Improved highlight overlay general functionality (for selecting nodes/edges). Previously selecting a node/edge had the program attempting to create an equal sized array as an overlay, find all objects corresponding to the selected ones, fill those into the new highlight overlay, then overlay that image. This was understandably quite slow in big arrays where the system was wasting a lot of time searching the entire array every time something was selected. New version retains this functionality for arrays below 125 million voxels, since search time is rather manageable at that size. For larger arrays, it instead draws the highlight for the selected objects only into the current slice, rendering a new slice whenever the user scrolls in the stack (although the entire highlight overlay is still initialized as a placeholder). Functions that require the use of the entire highlight overlay (such as masking) are correspondingly updated to draw the entirety of the highlight overlay before executing (when the system has up until that point been drawing slices one at a time). This will likely be the retained behavior moving forward, although to eliminate this behavior, one can open nettracer_gui.py and set self.mini_thresh to some comically large value. The new highlight overlay seems to work effectively the same but faster in my testing although it is possible a bug slipped through, which I will fix if informed about (or if I find it myself).
51
+ 2. For the random forrest segmenter, added the option to segment by 2D slice instead of 3D. In short, the 3D version was very greedy with its RAM usage and essentially computes a set of feature maps for the target array that are equivalent dims. By default it makes 7 of these, so a 1 GB array is going to demand 7GB of RAM to store its feature maps. The 2D slice version segments by 2D slice instead, meaning it stores only feature maps for slices with actual slices on them for training the model, and then for predicting voxel foreground/background, it computes the feature maps for the current slice (admittedly in the threadpool so this is amplified by core amount in the CPU but generally speaking this will save a lot of RAM). The reason I made this segmenter is to give NetTracer3D all the bells and whistles to let a user do everything in one program, but admittedly there are more efficient ML segmenters out there, such as through FIJI's big data viewer which is a lot better at handling big arrays in memory. I considered a way to compute 3D feature maps one at a time, save them in chunks to hard mem, and then open them while doing the chunked total segmentation but frankly this would likely be too slow to want to use so I will just suggest using 2D slice segmenter if RAM runs low.
52
52
 
53
- 3. For the machine learning segmenter, changed the system to attempt to segment the image by chunking the array into the largest possible chunks that can be divided across all CPU cores. Previously the system split the array into 64^3 voxel sized chunks and passed those to the CPU cores until everything was processed. I am not sure which version is more efficient/faster so this is somewhat of a test. In theory the new behavior could be faster because it asking Python to interpret less stuff.
53
+ I do not anticipate any major updates for this project for the foreseeable future beyond bug fixes.
@@ -0,0 +1,17 @@
1
+ NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity and volumetric thresholding, or random forest machine learning segmentation. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, enter the command 'nettracer3d' in your command prompt:
2
+
3
+
4
+ This gui is built from the PyQt6 package and therefore may not function on dockers or virtual envs that are unable to support PyQt6 displays. More advanced documentation is coming down the line, but for now please see: https://www.youtube.com/watch?v=cRatn5VTWDY
5
+ for a video tutorial on using the GUI.
6
+
7
+ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation is provided, and is available for commercial use at a fee (see license file for information).
8
+
9
+ NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
10
+
11
+ -- Version 0.5.5 updates --
12
+
13
+ 1. When the highlight overlay is being used in big overlays (and therefore defaulting to the mini overlay), updated system to just make one 2d overlay that regenerates on slice movement. Decided this was better than having it sit in a bigger 3d array when said array wasn't being used for anything and had to be rebuilt on slice movement or 3d operations anyway.
14
+
15
+ 2. For the random forrest segmenter, added the option to segment by 2D slice instead of 3D. In short, the 3D version was very greedy with its RAM usage and essentially computes a set of feature maps for the target array that are equivalent dims. By default it makes 7 of these, so a 1 GB array is going to demand 7GB of RAM to store its feature maps. The 2D slice version segments by 2D slice instead, meaning it stores only feature maps for slices with actual slices on them for training the model, and then for predicting voxel foreground/background, it computes the feature maps for the current slice (admittedly in the threadpool so this is amplified by core amount in the CPU but generally speaking this will save a lot of RAM). The reason I made this segmenter is to give NetTracer3D all the bells and whistles to let a user do everything in one program, but admittedly there are more efficient ML segmenters out there, such as through FIJI's big data viewer which is a lot better at handling big arrays in memory. I considered a way to compute 3D feature maps one at a time, save them in chunks to hard mem, and then open them while doing the chunked total segmentation but frankly this would likely be too slow to want to use so I will just suggest using 2D slice segmenter if RAM runs low.
16
+
17
+ I do not anticipate any major updates for this project for the foreseeable future beyond bug fixes.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.5.4"
3
+ version = "0.5.5"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="mclaughlinliam99@gmail.com" },
6
6
  ]
@@ -398,6 +398,7 @@ class ImageViewerWindow(QMainWindow):
398
398
  self.highlight_overlay = None
399
399
  self.highlight_bounds = None # Store bounds for positioning
400
400
  self.mini_overlay = False # If the program is currently drawing the overlay by frame this will be true
401
+ self.mini_overlay_data = None #Actual data for mini overlay
401
402
  self.mini_thresh = (500*500*500) # Array volume to start using mini overlays for
402
403
 
403
404
  def start_left_scroll(self):
@@ -446,6 +447,7 @@ class ImageViewerWindow(QMainWindow):
446
447
  """
447
448
 
448
449
  self.mini_overlay = False #If this method is ever being called, it means we are rendering the entire overlay so mini overlay needs to reset.
450
+ self.mini_overlay_data = None
449
451
 
450
452
  def process_chunk(chunk_data, indices_to_check):
451
453
  """Process a single chunk of the array to create highlight mask"""
@@ -562,6 +564,8 @@ class ImageViewerWindow(QMainWindow):
562
564
 
563
565
  def create_highlight_overlay_slice(self, indices, bounds = False):
564
566
 
567
+ """Highlight overlay generation method specific for the segmenter interactive mode"""
568
+
565
569
 
566
570
  def process_chunk_bounds(chunk_data, indices_to_check):
567
571
  """Process a single chunk of the array to create highlight mask"""
@@ -638,7 +642,7 @@ class ImageViewerWindow(QMainWindow):
638
642
  def create_mini_overlay(self, node_indices = None, edge_indices = None):
639
643
 
640
644
  """
641
- Create a binary overlay highlighting specific nodes and/or edges using parallel processing, one slice at a time for efficiency.
645
+ Create a highlight overlay one slice at a time.
642
646
 
643
647
  Args:
644
648
  node_indices (list): List of node indices to highlight
@@ -668,28 +672,29 @@ class ImageViewerWindow(QMainWindow):
668
672
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
669
673
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
670
674
 
671
- if not node_indices and not edge_indices:
672
- self.highlight_overlay = None
675
+ if not node_indices and not edge_indices: #Theoretically this can't be called because it uses full highlight overlay method for empty clicks
676
+ self.mini_overlay_data = None
677
+ self.mini_overlay = False
673
678
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
674
679
  return
675
680
 
676
- # Get the shape of the full array from any existing channel
681
+ # Get the shape of the mini array from any existing channel
677
682
  for channel in self.channel_data:
678
683
  if channel is not None:
679
684
  full_shape = channel.shape
685
+ full_shape = (full_shape[1], full_shape[2]) #Just get (Y, X) shape
680
686
  break
681
687
  else:
682
688
  return # No valid channels to get shape from
683
689
 
684
690
  # Initialize full-size overlay
685
- if self.highlight_overlay is None:
686
- self.highlight_overlay = np.zeros(full_shape, dtype=np.uint8)
691
+ self.mini_overlay_data = np.zeros(full_shape, dtype=np.uint8)
687
692
 
688
693
  # Get number of CPU cores
689
694
  num_cores = mp.cpu_count()
690
695
 
691
696
  # Calculate chunk size along y-axis
692
- chunk_size = full_shape[1] // num_cores
697
+ chunk_size = full_shape[0] // num_cores
693
698
  if chunk_size < 1:
694
699
  chunk_size = 1
695
700
 
@@ -699,8 +704,8 @@ class ImageViewerWindow(QMainWindow):
699
704
 
700
705
  # Create chunks
701
706
  chunks = []
702
- for i in range(0, array_shape[1], chunk_size):
703
- end = min(i + chunk_size, array_shape[1])
707
+ for i in range(0, array_shape[0], chunk_size):
708
+ end = min(i + chunk_size, array_shape[0])
704
709
  chunks.append(channel_data[i:end, :])
705
710
 
706
711
  # Process chunks in parallel using ThreadPoolExecutor
@@ -729,11 +734,10 @@ class ImageViewerWindow(QMainWindow):
729
734
  edge_overlay = None
730
735
 
731
736
  # Combine results
732
- self.highlight_overlay[self.current_slice, :, :] = np.zeros_like(self.highlight_overlay[self.current_slice, :, :])
733
737
  if node_overlay is not None:
734
- self.highlight_overlay[self.current_slice, :, :] = np.maximum(self.highlight_overlay[self.current_slice, :, :], node_overlay)
738
+ self.mini_overlay_data = np.maximum(self.mini_overlay_data, node_overlay)
735
739
  if edge_overlay is not None:
736
- self.highlight_overlay[self.current_slice, :, :] = np.maximum(self.highlight_overlay[self.current_slice, :, :], edge_overlay)
740
+ self.mini_overlay_data = np.maximum(self.mini_overlay_data, edge_overlay)
737
741
 
738
742
 
739
743
  # Update display
@@ -3488,6 +3492,8 @@ class ImageViewerWindow(QMainWindow):
3488
3492
  if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
3489
3493
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
3490
3494
  self.update_display(preserve_zoom=view_settings)
3495
+ if self.machine_window is not None:
3496
+ self.machine_window.poke_segmenter()
3491
3497
  self.pending_slice = None
3492
3498
 
3493
3499
  def update_brightness(self, channel_index, values):
@@ -3623,7 +3629,15 @@ class ImageViewerWindow(QMainWindow):
3623
3629
  self.create_highlight_overlay_slice(self.targs, bounds = self.bounds)
3624
3630
 
3625
3631
  # Add highlight overlay if it exists
3626
- if self.highlight_overlay is not None and self.highlight and self.machine_window is None:
3632
+ if self.mini_overlay and self.highlight and self.machine_window is None:
3633
+ highlight_cmap = LinearSegmentedColormap.from_list(
3634
+ 'highlight',
3635
+ [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
3636
+ )
3637
+ self.ax.imshow(self.mini_overlay_data,
3638
+ cmap=highlight_cmap,
3639
+ alpha=0.5)
3640
+ elif self.highlight_overlay is not None and self.highlight and self.machine_window is None:
3627
3641
  highlight_slice = self.highlight_overlay[self.current_slice]
3628
3642
  highlight_cmap = LinearSegmentedColormap.from_list(
3629
3643
  'highlight',
@@ -6202,7 +6216,18 @@ class MachineWindow(QMainWindow):
6202
6216
  self.GPU.setChecked(False)
6203
6217
  self.GPU.clicked.connect(self.toggle_GPU)
6204
6218
  self.use_gpu = False
6219
+ self.two = QPushButton("Train By 2D Slice Patterns (Cheaper - CPU only)")
6220
+ self.two.setCheckable(True)
6221
+ self.two.setChecked(True)
6222
+ self.two.clicked.connect(self.toggle_two)
6223
+ self.use_two = True
6224
+ self.three = QPushButton("Train by 3D Patterns")
6225
+ self.three.setCheckable(True)
6226
+ self.three.setChecked(False)
6227
+ self.three.clicked.connect(self.toggle_three)
6205
6228
  processing_layout.addWidget(self.GPU)
6229
+ processing_layout.addWidget(self.two)
6230
+ processing_layout.addWidget(self.three)
6206
6231
  processing_group.setLayout(processing_layout)
6207
6232
 
6208
6233
  # Group 3: Training Options
@@ -6220,6 +6245,7 @@ class MachineWindow(QMainWindow):
6220
6245
  segmentation_group = QGroupBox("Segmentation")
6221
6246
  segmentation_layout = QVBoxLayout()
6222
6247
  seg_button = QPushButton("Preview Segment")
6248
+ self.seg_button = seg_button
6223
6249
  seg_button.clicked.connect(self.start_segmentation)
6224
6250
  full_button = QPushButton("Segment All")
6225
6251
  full_button.clicked.connect(self.segment)
@@ -6237,9 +6263,12 @@ class MachineWindow(QMainWindow):
6237
6263
  self.setCentralWidget(main_widget)
6238
6264
 
6239
6265
  self.trained = False
6266
+ self.previewing = False
6240
6267
 
6241
6268
 
6242
6269
  self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=True)
6270
+ self.segmentation_worker = None
6271
+
6243
6272
 
6244
6273
 
6245
6274
  def toggle_GPU(self):
@@ -6247,6 +6276,27 @@ class MachineWindow(QMainWindow):
6247
6276
 
6248
6277
  self.use_gpu = self.GPU.isChecked()
6249
6278
 
6279
+ def toggle_two(self):
6280
+ if self.two.isChecked():
6281
+ # If button two is checked, ensure button three is unchecked
6282
+ self.three.setChecked(False)
6283
+ self.use_two = True
6284
+ else:
6285
+ # If button three is checked, ensure button two is unchecked
6286
+ self.three.setChecked(True)
6287
+ self.use_two = False
6288
+
6289
+ def toggle_three(self):
6290
+ if self.three.isChecked():
6291
+ # If button two is checked, ensure button three is unchecked
6292
+ self.two.setChecked(False)
6293
+ self.use_two = False
6294
+ else:
6295
+ # If button three is checked, ensure button two is unchecked
6296
+ self.two.setChecked(True)
6297
+ self.use_two = True
6298
+
6299
+
6250
6300
 
6251
6301
  def toggle_foreground(self):
6252
6302
 
@@ -6295,15 +6345,29 @@ class MachineWindow(QMainWindow):
6295
6345
  self.kill_segmentation()
6296
6346
  # Wait a bit for cleanup
6297
6347
  time.sleep(0.1)
6298
- self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu)
6299
- self.trained = True
6348
+ if not self.use_two:
6349
+ self.previewing = False
6350
+ try:
6351
+ self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu, use_two = self.use_two)
6352
+ self.trained = True
6353
+ except MemoryError:
6354
+ QMessageBox.critical(
6355
+ self,
6356
+ "Alert",
6357
+ "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."
6358
+ )
6359
+
6300
6360
 
6301
6361
  def start_segmentation(self):
6302
6362
 
6303
6363
  self.kill_segmentation()
6304
6364
  time.sleep(0.1)
6305
6365
 
6306
- print("Beginning new segmentation...")
6366
+ if self.use_two:
6367
+ self.previewing = True
6368
+ else:
6369
+ print("Beginning new segmentation...")
6370
+
6307
6371
 
6308
6372
  if self.parent().active_channel == 0:
6309
6373
  if self.parent().channel_data[0] is not None:
@@ -6317,7 +6381,7 @@ class MachineWindow(QMainWindow):
6317
6381
  if not self.trained:
6318
6382
  return
6319
6383
  else:
6320
- self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu)
6384
+ self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self)
6321
6385
  self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
6322
6386
  self.segmentation_worker.finished.connect(self.segmentation_finished)
6323
6387
  current_xlim = self.parent().ax.get_xlim()
@@ -6345,10 +6409,42 @@ class MachineWindow(QMainWindow):
6345
6409
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
6346
6410
  return msg.exec() == QMessageBox.StandardButton.Yes
6347
6411
 
6412
+
6413
+
6414
+ def check_for_z_change(self):
6415
+ current_z = self.parent().current_slice
6416
+ if not hasattr(self, '_last_z'):
6417
+ self._last_z = current_z
6418
+ return False
6419
+
6420
+ changed = (self._last_z != current_z)
6421
+ self._last_z = current_z
6422
+
6423
+ if changed and self.previewing and self.segmentation_worker is not None:
6424
+ self.segmentation_worker.stop()
6425
+ time.sleep(0.1)
6426
+
6427
+ # Force regeneration of chunks
6428
+ self.segmenter.realtimechunks = None
6429
+
6430
+ # Restart the worker
6431
+ self.start_segmentation()
6432
+
6433
+ return changed
6434
+
6348
6435
  def update_display(self):
6349
6436
  if not hasattr(self, '_last_update'):
6350
6437
  self._last_update = 0
6351
6438
 
6439
+ current_z = self.parent().current_slice
6440
+ if not hasattr(self, '_last_z'):
6441
+ self._last_z = current_z
6442
+
6443
+ self._last_z = current_z
6444
+
6445
+ if self.previewing:
6446
+ changed = self.check_for_z_change()
6447
+
6352
6448
  current_time = time.time()
6353
6449
  if current_time - self._last_update >= 1: # Match worker's interval
6354
6450
  try:
@@ -6366,19 +6462,84 @@ class MachineWindow(QMainWindow):
6366
6462
  except Exception as e:
6367
6463
  print(f"Display update error: {e}")
6368
6464
 
6465
+ def poke_segmenter(self):
6466
+ if self.use_two and self.previewing:
6467
+ try:
6468
+ # Clear any processing flags in the segmenter
6469
+ if hasattr(self.segmenter, '_currently_processing'):
6470
+ self.segmenter._currently_processing = None
6471
+
6472
+ # Force regenerating the worker
6473
+ if self.segmentation_worker is not None:
6474
+ self.kill_segmentation()
6475
+
6476
+ time.sleep(0.2)
6477
+ self.start_segmentation()
6478
+
6479
+ except Exception as e:
6480
+ print(f"Error in poke_segmenter: {e}")
6481
+ import traceback
6482
+ traceback.print_exc()
6483
+
6369
6484
  def segmentation_finished(self):
6370
- print("Segmentation completed")
6485
+ if not self.use_two:
6486
+ print("Segmentation completed")
6487
+
6371
6488
  current_xlim = self.parent().ax.get_xlim()
6372
6489
  current_ylim = self.parent().ax.get_ylim()
6373
6490
  self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
6491
+
6492
+ # Store the current z position before killing the worker
6493
+ current_z = self.parent().current_slice
6494
+
6495
+ # Clean up the worker
6374
6496
  self.kill_segmentation()
6497
+ self.segmentation_worker = None
6375
6498
  time.sleep(0.1)
6499
+
6500
+ # Auto-restart for 2D preview mode only if certain conditions are met
6501
+ if self.previewing and self.use_two:
6502
+ # Track when this slice was last processed
6503
+ if not hasattr(self, '_processed_slices'):
6504
+ self._processed_slices = {}
6505
+
6506
+ current_time = time.time()
6507
+
6508
+ # Check if we've recently tried to process this slice (to prevent loops)
6509
+ recently_processed = False
6510
+ if current_z in self._processed_slices:
6511
+ time_since_last_attempt = current_time - self._processed_slices[current_z]
6512
+ recently_processed = time_since_last_attempt < 5.0 # 5 second cooldown
6513
+
6514
+ if not recently_processed:
6515
+ self._processed_slices[current_z] = current_time
6516
+
6517
+ # Reset any processing flags in the segmenter
6518
+ if hasattr(self.segmenter, '_currently_processing'):
6519
+ self.segmenter._currently_processing = None
6520
+
6521
+ # Create a new worker after a brief delay
6522
+ QTimer.singleShot(500, self.start_segmentation)
6523
+
6376
6524
 
6377
6525
 
6378
6526
  def kill_segmentation(self):
6379
- if hasattr(self, 'segmentation_worker'):
6527
+ if hasattr(self, 'segmentation_worker') and self.segmentation_worker is not None:
6528
+ # Signal the thread to stop
6380
6529
  self.segmentation_worker.stop()
6381
- del self.segmentation_worker # Clean up reference
6530
+
6531
+ # Wait for the thread to finish
6532
+ if self.segmentation_worker.isRunning():
6533
+ self.segmentation_worker.wait(1000) # Wait up to 1 second
6534
+
6535
+ # If thread is still running after timeout, try to force termination
6536
+ if self.segmentation_worker.isRunning():
6537
+ self.segmentation_worker.terminate()
6538
+ self.segmentation_worker.wait() # Wait for it to be terminated
6539
+
6540
+ # Now safe to delete
6541
+ del self.segmentation_worker
6542
+ self.segmentation_worker = None
6382
6543
 
6383
6544
 
6384
6545
  def segment(self):
@@ -6391,6 +6552,8 @@ class MachineWindow(QMainWindow):
6391
6552
  self.kill_segmentation()
6392
6553
  time.sleep(0.1)
6393
6554
 
6555
+ self.previewing = False
6556
+
6394
6557
  if self.parent().active_channel == 0:
6395
6558
  if self.parent().channel_data[0] is not None:
6396
6559
  active_data = self.parent().channel_data[0]
@@ -6420,20 +6583,26 @@ class MachineWindow(QMainWindow):
6420
6583
 
6421
6584
  self.parent().update_display()
6422
6585
 
6586
+ self.previewing = False
6587
+
6423
6588
  print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
6424
6589
 
6425
6590
  def closeEvent(self, event):
6426
6591
  if self.parent().isVisible():
6427
6592
  if self.confirm_close_dialog():
6428
-
6593
+ # Clean up resources before closing
6429
6594
  if self.brush_button.isChecked():
6430
6595
  self.silence_button()
6431
6596
  self.toggle_brush_mode()
6597
+
6432
6598
  self.parent().pen_button.setEnabled(True)
6433
6599
  self.parent().brush_mode = False
6434
- self.parent().machine_window = None
6600
+
6601
+ # Kill the segmentation thread and wait for it to finish
6435
6602
  self.kill_segmentation()
6436
- time.sleep(0.1)
6603
+ time.sleep(0.2) # Give additional time for cleanup
6604
+
6605
+ self.parent().machine_window = None
6437
6606
  else:
6438
6607
  event.ignore()
6439
6608
 
@@ -6444,48 +6613,84 @@ class SegmentationWorker(QThread):
6444
6613
  finished = pyqtSignal()
6445
6614
  chunk_processed = pyqtSignal()
6446
6615
 
6447
- def __init__(self, highlight_overlay, segmenter, use_gpu):
6616
+ def __init__(self, highlight_overlay, segmenter, use_gpu, use_two, previewing, machine_window):
6448
6617
  super().__init__()
6449
6618
  self.overlay = highlight_overlay
6450
6619
  self.segmenter = segmenter
6451
6620
  self.use_gpu = use_gpu
6621
+ self.use_two = use_two
6622
+ self.previewing = previewing
6623
+ self.machine_window = machine_window
6452
6624
  self._stop = False
6453
6625
  self.update_interval = 1 # Increased to 500ms
6454
6626
  self.chunks_since_update = 0
6455
6627
  self.chunks_per_update = 5 # Only update every 5 chunks
6628
+ self.poked = False # If it should wake up or not
6456
6629
  self.last_update = time.time()
6457
6630
 
6458
6631
  def stop(self):
6459
6632
  self._stop = True
6633
+
6634
+ def get_poked(self):
6635
+ self.poked = True
6460
6636
 
6461
6637
  def run(self):
6462
6638
  try:
6463
6639
  self.overlay.fill(False)
6464
6640
 
6465
- for foreground_coords, background_coords in self.segmenter.segment_volume_realtime(gpu = self.use_gpu):
6466
- if self._stop:
6467
- break
6641
+ # Remember the starting z position
6642
+ self.starting_z = self.segmenter.current_z
6643
+
6644
+ if self.previewing and self.use_two:
6645
+ # Process current z-slice in chunks
6646
+ current_z = self.segmenter.current_z
6647
+
6648
+ # Process the slice with chunked generator
6649
+ for foreground, background in self.segmenter.segment_slice_chunked(current_z):
6650
+ if self._stop:
6651
+ break
6468
6652
 
6469
- for z,y,x in foreground_coords:
6470
- self.overlay[z,y,x] = 1
6471
- for z,y,x in background_coords:
6472
- self.overlay[z,y,x] = 2
6473
-
6474
- # Update only after several chunks AND minimum time interval
6475
- self.chunks_since_update += 1
6476
- current_time = time.time()
6477
- if (self.chunks_since_update >= self.chunks_per_update and
6478
- current_time - self.last_update >= self.update_interval):
6479
- self.chunk_processed.emit()
6480
- self.chunks_since_update = 0
6481
- self.last_update = current_time
6482
-
6653
+ # Update the overlay
6654
+ for z,y,x in foreground:
6655
+ self.overlay[z,y,x] = 1
6656
+ for z,y,x in background:
6657
+ self.overlay[z,y,x] = 2
6658
+
6659
+ # Signal update after each chunk
6660
+ self.chunks_since_update += 1
6661
+ current_time = time.time()
6662
+ if (self.chunks_since_update >= self.chunks_per_update and
6663
+ current_time - self.last_update >= self.update_interval):
6664
+ self.chunk_processed.emit()
6665
+ self.chunks_since_update = 0
6666
+ self.last_update = current_time
6667
+
6668
+ else:
6669
+ # Original 3D approach
6670
+ for foreground_coords, background_coords in self.segmenter.segment_volume_realtime(gpu=self.use_gpu):
6671
+ if self._stop:
6672
+ break
6673
+
6674
+ for z,y,x in foreground_coords:
6675
+ self.overlay[z,y,x] = 1
6676
+ for z,y,x in background_coords:
6677
+ self.overlay[z,y,x] = 2
6678
+
6679
+ self.chunks_since_update += 1
6680
+ current_time = time.time()
6681
+ if (self.chunks_since_update >= self.chunks_per_update and
6682
+ current_time - self.last_update >= self.update_interval):
6683
+ self.chunk_processed.emit()
6684
+ self.chunks_since_update = 0
6685
+ self.last_update = current_time
6686
+
6483
6687
  self.finished.emit()
6484
6688
 
6485
6689
  except Exception as e:
6486
6690
  print(f"Error in segmentation: {e}")
6487
- raise
6488
-
6691
+ import traceback
6692
+ traceback.print_exc()
6693
+
6489
6694
  def run_batch(self):
6490
6695
  try:
6491
6696
  foreground_coords, _ = self.segmenter.segment_volume()