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.
- {nettracer3d-0.5.4/src/nettracer3d.egg-info → nettracer3d-0.5.5}/PKG-INFO +5 -5
- nettracer3d-0.5.5/README.md +17 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/pyproject.toml +1 -1
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/nettracer_gui.py +248 -43
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/segmenter.py +576 -79
- {nettracer3d-0.5.4 → nettracer3d-0.5.5/src/nettracer3d.egg-info}/PKG-INFO +5 -5
- nettracer3d-0.5.4/README.md +0 -17
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/LICENSE +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/setup.cfg +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/hub_getter.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/nettracer.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-0.5.4 → nettracer3d-0.5.5}/src/nettracer3d.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
47
|
+
-- Version 0.5.5 updates --
|
|
48
48
|
|
|
49
|
-
1.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
@@ -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]
|
|
@@ -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
|
-
|
|
6600
|
+
|
|
6601
|
+
# Kill the segmentation thread and wait for it to finish
|
|
6435
6602
|
self.kill_segmentation()
|
|
6436
|
-
time.sleep(0.
|
|
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
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
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
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
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
|
-
|
|
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()
|