nettracer3d 0.5.4__tar.gz → 0.5.6__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.
- {nettracer3d-0.5.4/src/nettracer3d.egg-info → nettracer3d-0.5.6}/PKG-INFO +3 -7
- nettracer3d-0.5.6/README.md +13 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/pyproject.toml +1 -1
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/nettracer_gui.py +254 -45
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/segmenter.py +581 -80
- {nettracer3d-0.5.4 → nettracer3d-0.5.6/src/nettracer3d.egg-info}/PKG-INFO +3 -7
- nettracer3d-0.5.4/README.md +0 -17
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/LICENSE +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/setup.cfg +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/hub_getter.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/nettracer.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/src/nettracer3d.egg-info/requires.txt +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.6}/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.
|
|
3
|
+
Version: 0.5.6
|
|
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,6 @@ 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.
|
|
47
|
+
-- Version 0.5.6 updates --
|
|
48
48
|
|
|
49
|
-
1.
|
|
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).
|
|
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.
|
|
49
|
+
1. Minor change - Updated some of the overhead and post-random forrest voxel assignment for the segmenter to be faster. The major bottleneck for this is still querying sklearn which seems a bit harder to work around.
|
|
@@ -0,0 +1,13 @@
|
|
|
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.6 updates --
|
|
12
|
+
|
|
13
|
+
1. Minor change - Updated some of the overhead and post-random forrest voxel assignment for the segmenter to be faster. The major bottleneck for this is still querying sklearn which seems a bit harder to work around.
|
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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[
|
|
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[
|
|
703
|
-
end = min(i + chunk_size, array_shape[
|
|
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.
|
|
738
|
+
self.mini_overlay_data = np.maximum(self.mini_overlay_data, node_overlay)
|
|
735
739
|
if edge_overlay is not None:
|
|
736
|
-
self.
|
|
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.
|
|
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
|
-
|
|
6299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
@@ -6406,8 +6569,12 @@ class MachineWindow(QMainWindow):
|
|
|
6406
6569
|
# Clean up when done
|
|
6407
6570
|
self.segmenter.cleanup()
|
|
6408
6571
|
|
|
6409
|
-
|
|
6410
|
-
|
|
6572
|
+
fg_array = np.array(list(foreground_coords))
|
|
6573
|
+
if len(fg_array) > 0: # Check if we have any foreground coordinates
|
|
6574
|
+
# Unpack into separate coordinate arrays
|
|
6575
|
+
z_coords, y_coords, x_coords = fg_array[:, 0], fg_array[:, 1], fg_array[:, 2]
|
|
6576
|
+
# Assign values in a single vectorized operation
|
|
6577
|
+
self.parent().highlight_overlay[z_coords, y_coords, x_coords] = 255
|
|
6411
6578
|
|
|
6412
6579
|
self.parent().load_channel(3, self.parent().highlight_overlay, True)
|
|
6413
6580
|
|
|
@@ -6420,20 +6587,26 @@ class MachineWindow(QMainWindow):
|
|
|
6420
6587
|
|
|
6421
6588
|
self.parent().update_display()
|
|
6422
6589
|
|
|
6590
|
+
self.previewing = False
|
|
6591
|
+
|
|
6423
6592
|
print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
|
|
6424
6593
|
|
|
6425
6594
|
def closeEvent(self, event):
|
|
6426
6595
|
if self.parent().isVisible():
|
|
6427
6596
|
if self.confirm_close_dialog():
|
|
6428
|
-
|
|
6597
|
+
# Clean up resources before closing
|
|
6429
6598
|
if self.brush_button.isChecked():
|
|
6430
6599
|
self.silence_button()
|
|
6431
6600
|
self.toggle_brush_mode()
|
|
6601
|
+
|
|
6432
6602
|
self.parent().pen_button.setEnabled(True)
|
|
6433
6603
|
self.parent().brush_mode = False
|
|
6434
|
-
|
|
6604
|
+
|
|
6605
|
+
# Kill the segmentation thread and wait for it to finish
|
|
6435
6606
|
self.kill_segmentation()
|
|
6436
|
-
time.sleep(0.
|
|
6607
|
+
time.sleep(0.2) # Give additional time for cleanup
|
|
6608
|
+
|
|
6609
|
+
self.parent().machine_window = None
|
|
6437
6610
|
else:
|
|
6438
6611
|
event.ignore()
|
|
6439
6612
|
|
|
@@ -6444,48 +6617,84 @@ class SegmentationWorker(QThread):
|
|
|
6444
6617
|
finished = pyqtSignal()
|
|
6445
6618
|
chunk_processed = pyqtSignal()
|
|
6446
6619
|
|
|
6447
|
-
def __init__(self, highlight_overlay, segmenter, use_gpu):
|
|
6620
|
+
def __init__(self, highlight_overlay, segmenter, use_gpu, use_two, previewing, machine_window):
|
|
6448
6621
|
super().__init__()
|
|
6449
6622
|
self.overlay = highlight_overlay
|
|
6450
6623
|
self.segmenter = segmenter
|
|
6451
6624
|
self.use_gpu = use_gpu
|
|
6625
|
+
self.use_two = use_two
|
|
6626
|
+
self.previewing = previewing
|
|
6627
|
+
self.machine_window = machine_window
|
|
6452
6628
|
self._stop = False
|
|
6453
6629
|
self.update_interval = 1 # Increased to 500ms
|
|
6454
6630
|
self.chunks_since_update = 0
|
|
6455
6631
|
self.chunks_per_update = 5 # Only update every 5 chunks
|
|
6632
|
+
self.poked = False # If it should wake up or not
|
|
6456
6633
|
self.last_update = time.time()
|
|
6457
6634
|
|
|
6458
6635
|
def stop(self):
|
|
6459
6636
|
self._stop = True
|
|
6637
|
+
|
|
6638
|
+
def get_poked(self):
|
|
6639
|
+
self.poked = True
|
|
6460
6640
|
|
|
6461
6641
|
def run(self):
|
|
6462
6642
|
try:
|
|
6463
6643
|
self.overlay.fill(False)
|
|
6464
6644
|
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6645
|
+
# Remember the starting z position
|
|
6646
|
+
self.starting_z = self.segmenter.current_z
|
|
6647
|
+
|
|
6648
|
+
if self.previewing and self.use_two:
|
|
6649
|
+
# Process current z-slice in chunks
|
|
6650
|
+
current_z = self.segmenter.current_z
|
|
6651
|
+
|
|
6652
|
+
# Process the slice with chunked generator
|
|
6653
|
+
for foreground, background in self.segmenter.segment_slice_chunked(current_z):
|
|
6654
|
+
if self._stop:
|
|
6655
|
+
break
|
|
6468
6656
|
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6657
|
+
# Update the overlay
|
|
6658
|
+
for z,y,x in foreground:
|
|
6659
|
+
self.overlay[z,y,x] = 1
|
|
6660
|
+
for z,y,x in background:
|
|
6661
|
+
self.overlay[z,y,x] = 2
|
|
6662
|
+
|
|
6663
|
+
# Signal update after each chunk
|
|
6664
|
+
self.chunks_since_update += 1
|
|
6665
|
+
current_time = time.time()
|
|
6666
|
+
if (self.chunks_since_update >= self.chunks_per_update and
|
|
6667
|
+
current_time - self.last_update >= self.update_interval):
|
|
6668
|
+
self.chunk_processed.emit()
|
|
6669
|
+
self.chunks_since_update = 0
|
|
6670
|
+
self.last_update = current_time
|
|
6671
|
+
|
|
6672
|
+
else:
|
|
6673
|
+
# Original 3D approach
|
|
6674
|
+
for foreground_coords, background_coords in self.segmenter.segment_volume_realtime(gpu=self.use_gpu):
|
|
6675
|
+
if self._stop:
|
|
6676
|
+
break
|
|
6677
|
+
|
|
6678
|
+
for z,y,x in foreground_coords:
|
|
6679
|
+
self.overlay[z,y,x] = 1
|
|
6680
|
+
for z,y,x in background_coords:
|
|
6681
|
+
self.overlay[z,y,x] = 2
|
|
6682
|
+
|
|
6683
|
+
self.chunks_since_update += 1
|
|
6684
|
+
current_time = time.time()
|
|
6685
|
+
if (self.chunks_since_update >= self.chunks_per_update and
|
|
6686
|
+
current_time - self.last_update >= self.update_interval):
|
|
6687
|
+
self.chunk_processed.emit()
|
|
6688
|
+
self.chunks_since_update = 0
|
|
6689
|
+
self.last_update = current_time
|
|
6690
|
+
|
|
6483
6691
|
self.finished.emit()
|
|
6484
6692
|
|
|
6485
6693
|
except Exception as e:
|
|
6486
6694
|
print(f"Error in segmentation: {e}")
|
|
6487
|
-
|
|
6488
|
-
|
|
6695
|
+
import traceback
|
|
6696
|
+
traceback.print_exc()
|
|
6697
|
+
|
|
6489
6698
|
def run_batch(self):
|
|
6490
6699
|
try:
|
|
6491
6700
|
foreground_coords, _ = self.segmenter.segment_volume()
|