nettracer3d 0.6.2__py3-none-any.whl → 0.6.4__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 +45 -12
- nettracer3d/nettracer_gui.py +253 -31
- nettracer3d/network_analysis.py +6 -6
- nettracer3d/segmenter.py +5 -1
- {nettracer3d-0.6.2.dist-info → nettracer3d-0.6.4.dist-info}/METADATA +13 -5
- {nettracer3d-0.6.2.dist-info → nettracer3d-0.6.4.dist-info}/RECORD +10 -10
- {nettracer3d-0.6.2.dist-info → nettracer3d-0.6.4.dist-info}/WHEEL +1 -1
- {nettracer3d-0.6.2.dist-info → nettracer3d-0.6.4.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.6.2.dist-info → nettracer3d-0.6.4.dist-info/licenses}/LICENSE +0 -0
- {nettracer3d-0.6.2.dist-info → nettracer3d-0.6.4.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer.py
CHANGED
|
@@ -751,6 +751,8 @@ def fill_holes_3d(array):
|
|
|
751
751
|
|
|
752
752
|
return holes_mask
|
|
753
753
|
|
|
754
|
+
print("Filling Holes...")
|
|
755
|
+
|
|
754
756
|
array = binarize(array)
|
|
755
757
|
inv_array = invert_array(array)
|
|
756
758
|
|
|
@@ -3270,7 +3272,10 @@ class Network_3D:
|
|
|
3270
3272
|
self._xy_scale = xy_scale
|
|
3271
3273
|
self._z_scale = z_scale
|
|
3272
3274
|
|
|
3273
|
-
|
|
3275
|
+
try:
|
|
3276
|
+
self.save_scaling(directory)
|
|
3277
|
+
except:
|
|
3278
|
+
pass
|
|
3274
3279
|
|
|
3275
3280
|
if search is None and ignore_search_region == False:
|
|
3276
3281
|
search = 0
|
|
@@ -3286,29 +3291,51 @@ class Network_3D:
|
|
|
3286
3291
|
if other_nodes is not None:
|
|
3287
3292
|
self.merge_nodes(other_nodes, label_nodes)
|
|
3288
3293
|
|
|
3289
|
-
|
|
3290
|
-
|
|
3294
|
+
try:
|
|
3295
|
+
self.save_nodes(directory)
|
|
3296
|
+
except:
|
|
3297
|
+
pass
|
|
3298
|
+
try:
|
|
3299
|
+
self.save_node_identities(directory)
|
|
3300
|
+
except:
|
|
3301
|
+
pass
|
|
3291
3302
|
|
|
3292
3303
|
if not ignore_search_region:
|
|
3293
3304
|
self.calculate_search_region(search, GPU = GPU, fast_dil = fast_dil, GPU_downsample = GPU_downsample)
|
|
3294
|
-
self._nodes = None
|
|
3305
|
+
#self._nodes = None # I originally put this here to micromanage RAM a little bit (it writes it to disk so I wanted to purge it from mem briefly but now idt thats necessary and I'd rather give it flexibility when lacking write permissions)
|
|
3295
3306
|
search = None
|
|
3296
|
-
|
|
3307
|
+
try:
|
|
3308
|
+
self.save_search_region(directory)
|
|
3309
|
+
except:
|
|
3310
|
+
pass
|
|
3297
3311
|
|
|
3298
3312
|
self.calculate_edges(edges, diledge = diledge, inners = inners, hash_inner_edges = hash_inners, search = search, remove_edgetrunk = remove_trunk, GPU = GPU, fast_dil = fast_dil, skeletonized = skeletonize)
|
|
3299
3313
|
del edges
|
|
3300
|
-
|
|
3314
|
+
try:
|
|
3315
|
+
self.save_edges(directory)
|
|
3316
|
+
except:
|
|
3317
|
+
pass
|
|
3301
3318
|
|
|
3302
3319
|
self.calculate_network(search = search, ignore_search_region = ignore_search_region)
|
|
3303
|
-
|
|
3320
|
+
|
|
3321
|
+
try:
|
|
3322
|
+
self.save_network(directory)
|
|
3323
|
+
except:
|
|
3324
|
+
pass
|
|
3304
3325
|
|
|
3305
3326
|
if self._nodes is None:
|
|
3306
3327
|
self.load_nodes(directory)
|
|
3307
3328
|
|
|
3308
3329
|
self.calculate_node_centroids(down_factor)
|
|
3309
|
-
|
|
3330
|
+
try:
|
|
3331
|
+
self.save_node_centroids(directory)
|
|
3332
|
+
except:
|
|
3333
|
+
pass
|
|
3310
3334
|
self.calculate_edge_centroids(down_factor)
|
|
3311
|
-
|
|
3335
|
+
try:
|
|
3336
|
+
self.save_edge_centroids(directory)
|
|
3337
|
+
except:
|
|
3338
|
+
pass
|
|
3312
3339
|
|
|
3313
3340
|
|
|
3314
3341
|
def draw_network(self, directory = None, down_factor = None, GPU = False):
|
|
@@ -3557,15 +3584,21 @@ class Network_3D:
|
|
|
3557
3584
|
list1 = self._network_lists[0] #Get network lists to change
|
|
3558
3585
|
list2 = self._network_lists[1]
|
|
3559
3586
|
list3 = self._network_lists[2]
|
|
3587
|
+
return1 = []
|
|
3588
|
+
return2 = []
|
|
3589
|
+
return3 = []
|
|
3560
3590
|
|
|
3561
3591
|
for i in range(len(list1)):
|
|
3562
3592
|
list1[i] = self.communities[list1[i]] #Set node at network list spot to its community instead
|
|
3563
3593
|
list2[i] = self.communities[list2[i]]
|
|
3564
|
-
if list1[i]
|
|
3565
|
-
|
|
3594
|
+
if list1[i] != list2[i]: #Avoid self - self connections
|
|
3595
|
+
return1.append(list1[i])
|
|
3596
|
+
return2.append(list2[i])
|
|
3597
|
+
return3.append(list3[i])
|
|
3598
|
+
|
|
3566
3599
|
|
|
3567
3600
|
|
|
3568
|
-
self.network_lists = [
|
|
3601
|
+
self.network_lists = [return1, return2, return3]
|
|
3569
3602
|
|
|
3570
3603
|
if self._nodes is not None:
|
|
3571
3604
|
self._nodes = update_array(self._nodes, inverted, targets = targets) #Set the array to match the new network
|
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -325,7 +325,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
325
325
|
self.tabbed_data = TabbedDataWidget(self)
|
|
326
326
|
right_layout.addWidget(self.tabbed_data)
|
|
327
327
|
# Initialize data_table property to None - it will be set when tabs are added
|
|
328
|
-
self.data_table =
|
|
328
|
+
self.data_table = []
|
|
329
329
|
|
|
330
330
|
# Create table control panel
|
|
331
331
|
table_control = QWidget()
|
|
@@ -1814,6 +1814,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1814
1814
|
new_xlim = [xdata - x_range, xdata + x_range]
|
|
1815
1815
|
new_ylim = [ydata - y_range, ydata + y_range]
|
|
1816
1816
|
|
|
1817
|
+
|
|
1817
1818
|
if (new_xlim[0] <= self.original_xlim[0] or
|
|
1818
1819
|
new_xlim[1] >= self.original_xlim[1] or
|
|
1819
1820
|
new_ylim[0] <= self.original_ylim[0] or
|
|
@@ -2429,10 +2430,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2429
2430
|
binarize_action.triggered.connect(self.show_binarize_dialog)
|
|
2430
2431
|
label_action = image_menu.addAction("Label Objects")
|
|
2431
2432
|
label_action.triggered.connect(self.show_label_dialog)
|
|
2433
|
+
slabel_action = image_menu.addAction("Neighborhood Labels")
|
|
2434
|
+
slabel_action.triggered.connect(self.show_slabel_dialog)
|
|
2432
2435
|
thresh_action = image_menu.addAction("Threshold/Segment")
|
|
2433
2436
|
thresh_action.triggered.connect(self.show_thresh_dialog)
|
|
2434
2437
|
mask_action = image_menu.addAction("Mask Channel")
|
|
2435
2438
|
mask_action.triggered.connect(self.show_mask_dialog)
|
|
2439
|
+
type_action = image_menu.addAction("Channel dtype")
|
|
2440
|
+
type_action.triggered.connect(self.show_type_dialog)
|
|
2436
2441
|
skeletonize_action = image_menu.addAction("Skeletonize")
|
|
2437
2442
|
skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
|
|
2438
2443
|
watershed_action = image_menu.addAction("Watershed")
|
|
@@ -2657,6 +2662,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2657
2662
|
dialog = LabelDialog(self)
|
|
2658
2663
|
dialog.exec()
|
|
2659
2664
|
|
|
2665
|
+
def show_slabel_dialog(self):
|
|
2666
|
+
"""Show the slabel dialog"""
|
|
2667
|
+
dialog = SLabelDialog(self)
|
|
2668
|
+
dialog.exec()
|
|
2669
|
+
|
|
2660
2670
|
def show_thresh_dialog(self):
|
|
2661
2671
|
"""Show threshold dialog"""
|
|
2662
2672
|
if self.machine_window is not None:
|
|
@@ -2671,6 +2681,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2671
2681
|
dialog = MaskDialog(self)
|
|
2672
2682
|
dialog.exec()
|
|
2673
2683
|
|
|
2684
|
+
def show_type_dialog(self):
|
|
2685
|
+
"""Show the type dialog"""
|
|
2686
|
+
dialog = TypeDialog(self)
|
|
2687
|
+
dialog.exec()
|
|
2688
|
+
|
|
2674
2689
|
def show_skeletonize_dialog(self):
|
|
2675
2690
|
"""show the skeletonize dialog"""
|
|
2676
2691
|
dialog = SkeletonizeDialog(self)
|
|
@@ -3066,6 +3081,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3066
3081
|
def set_active_channel(self, index):
|
|
3067
3082
|
"""Set the active channel and update UI accordingly."""
|
|
3068
3083
|
self.active_channel = index
|
|
3084
|
+
self.active_channel_combo.setCurrentIndex(index)
|
|
3069
3085
|
# Update button appearances to show active channel
|
|
3070
3086
|
for i, btn in enumerate(self.channel_buttons):
|
|
3071
3087
|
if i == index and btn.isEnabled():
|
|
@@ -3213,6 +3229,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3213
3229
|
self.highlight_overlay = None
|
|
3214
3230
|
except:
|
|
3215
3231
|
pass
|
|
3232
|
+
if not data:
|
|
3233
|
+
self.original_xlim = None
|
|
3234
|
+
self.original_ylim = None
|
|
3216
3235
|
continue
|
|
3217
3236
|
else:
|
|
3218
3237
|
old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
|
|
@@ -3972,7 +3991,7 @@ class CustomTableView(QTableView):
|
|
|
3972
3991
|
desc_action.triggered.connect(lambda checked, c=col: self.sort_table(c, ascending=False))
|
|
3973
3992
|
|
|
3974
3993
|
# Different menus for top and bottom tables
|
|
3975
|
-
if self
|
|
3994
|
+
if self in self.parent.data_table: # Top table
|
|
3976
3995
|
save_menu = context_menu.addMenu("Save As")
|
|
3977
3996
|
save_csv = save_menu.addAction("CSV")
|
|
3978
3997
|
save_excel = save_menu.addAction("Excel")
|
|
@@ -4081,7 +4100,7 @@ class CustomTableView(QTableView):
|
|
|
4081
4100
|
df = self.model()._data
|
|
4082
4101
|
|
|
4083
4102
|
# Get table name for the file dialog title
|
|
4084
|
-
if self
|
|
4103
|
+
if self in self.parent.data_table:
|
|
4085
4104
|
table_name = "Statistics"
|
|
4086
4105
|
elif self == self.parent.network_table:
|
|
4087
4106
|
table_name = "Network"
|
|
@@ -4090,7 +4109,7 @@ class CustomTableView(QTableView):
|
|
|
4090
4109
|
|
|
4091
4110
|
# Get save file name
|
|
4092
4111
|
file_filter = ("CSV Files (*.csv)" if file_type == 'csv' else
|
|
4093
|
-
"Excel Files (*.xlsx)" if file_type == '
|
|
4112
|
+
"Excel Files (*.xlsx)" if file_type == 'xlsx' else
|
|
4094
4113
|
"Gephi Graph (*.gexf)" if file_type == 'gexf' else
|
|
4095
4114
|
"GraphML (*.graphml)" if file_type == 'graphml' else
|
|
4096
4115
|
"Pajek Network (*.net)")
|
|
@@ -4475,7 +4494,13 @@ class TabbedDataWidget(QTabWidget):
|
|
|
4475
4494
|
"""Add a new table with the given name"""
|
|
4476
4495
|
if name in self.tables:
|
|
4477
4496
|
# If tab already exists, update its content
|
|
4478
|
-
|
|
4497
|
+
old_table = self.tables[name]
|
|
4498
|
+
idx = self.indexOf(old_table)
|
|
4499
|
+
|
|
4500
|
+
# Remove the old table reference from parent's data_table
|
|
4501
|
+
if self.parent_window and old_table in self.parent_window.data_table:
|
|
4502
|
+
self.parent_window.data_table.remove(old_table)
|
|
4503
|
+
|
|
4479
4504
|
self.removeTab(idx)
|
|
4480
4505
|
|
|
4481
4506
|
# Create a new CustomTableView with is_top_table=True
|
|
@@ -4495,7 +4520,7 @@ class TabbedDataWidget(QTabWidget):
|
|
|
4495
4520
|
|
|
4496
4521
|
# Update parent's data_table reference
|
|
4497
4522
|
if self.parent_window:
|
|
4498
|
-
self.parent_window.data_table
|
|
4523
|
+
self.parent_window.data_table.append(new_table)
|
|
4499
4524
|
|
|
4500
4525
|
def close_tab(self, index):
|
|
4501
4526
|
"""Close the tab at the given index"""
|
|
@@ -4510,12 +4535,12 @@ class TabbedDataWidget(QTabWidget):
|
|
|
4510
4535
|
if name_to_remove:
|
|
4511
4536
|
del self.tables[name_to_remove]
|
|
4512
4537
|
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
if self.parent_window and self.count() > 0:
|
|
4517
|
-
self.parent_window.data_table = self.currentWidget()
|
|
4538
|
+
# Update parent's data_table reference by removing the widget
|
|
4539
|
+
if self.parent_window and widget in self.parent_window.data_table:
|
|
4540
|
+
self.parent_window.data_table.remove(widget)
|
|
4518
4541
|
|
|
4542
|
+
self.removeTab(index)
|
|
4543
|
+
|
|
4519
4544
|
def clear_all_tabs(self):
|
|
4520
4545
|
"""Remove all tabs"""
|
|
4521
4546
|
while self.count() > 0:
|
|
@@ -5418,18 +5443,23 @@ class RadialDialog(QDialog):
|
|
|
5418
5443
|
|
|
5419
5444
|
def radial(self):
|
|
5420
5445
|
|
|
5421
|
-
|
|
5446
|
+
try:
|
|
5422
5447
|
|
|
5423
|
-
|
|
5448
|
+
distance = float(self.distance.text()) if self.distance.text().strip() else 50
|
|
5424
5449
|
|
|
5425
|
-
|
|
5426
|
-
self.parent().show_centroid_dialog()
|
|
5450
|
+
directory = str(self.distance.text()) if self.directory.text().strip() else None
|
|
5427
5451
|
|
|
5428
|
-
|
|
5452
|
+
if my_network.node_centroids is None:
|
|
5453
|
+
self.parent().show_centroid_dialog()
|
|
5429
5454
|
|
|
5430
|
-
|
|
5455
|
+
radial = my_network.radial_distribution(distance, directory = directory)
|
|
5431
5456
|
|
|
5432
|
-
|
|
5457
|
+
self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
|
|
5458
|
+
|
|
5459
|
+
self.accept()
|
|
5460
|
+
|
|
5461
|
+
except Exception as e:
|
|
5462
|
+
print(f"An error occurred: {e}")
|
|
5433
5463
|
|
|
5434
5464
|
class DegreeDistDialog(QDialog):
|
|
5435
5465
|
|
|
@@ -5462,7 +5492,7 @@ class DegreeDistDialog(QDialog):
|
|
|
5462
5492
|
|
|
5463
5493
|
self.accept()
|
|
5464
5494
|
|
|
5465
|
-
except
|
|
5495
|
+
except Exception as e:
|
|
5466
5496
|
print(f"An error occurred: {e}")
|
|
5467
5497
|
|
|
5468
5498
|
class NeighborIdentityDialog(QDialog):
|
|
@@ -6261,6 +6291,91 @@ class LabelDialog(QDialog):
|
|
|
6261
6291
|
f"Error running label: {str(e)}"
|
|
6262
6292
|
)
|
|
6263
6293
|
|
|
6294
|
+
|
|
6295
|
+
class SLabelDialog(QDialog):
|
|
6296
|
+
def __init__(self, parent=None):
|
|
6297
|
+
super().__init__(parent)
|
|
6298
|
+
self.setWindowTitle("Smart Label (Use label array to assign label neighborhoods to binary array)?")
|
|
6299
|
+
self.setModal(True)
|
|
6300
|
+
|
|
6301
|
+
layout = QFormLayout(self)
|
|
6302
|
+
|
|
6303
|
+
|
|
6304
|
+
# Add mode selection dropdown
|
|
6305
|
+
self.mode_selector = QComboBox()
|
|
6306
|
+
self.mode_selector.addItems(["Nodes", "Edges", "Overlay 1", "Overlay 2"])
|
|
6307
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
6308
|
+
layout.addRow("Prelabeled Array:", self.mode_selector)
|
|
6309
|
+
|
|
6310
|
+
layout.addRow(QLabel("Will Label Neighborhoods in: "))
|
|
6311
|
+
|
|
6312
|
+
# Add mode selection dropdown
|
|
6313
|
+
self.target_selector = QComboBox()
|
|
6314
|
+
self.target_selector.addItems(["Nodes", "Edges", "Overlay 1", "Overlay 2"])
|
|
6315
|
+
self.target_selector.setCurrentIndex(1) # Default to Mode 1
|
|
6316
|
+
layout.addRow("Binary Array:", self.target_selector)
|
|
6317
|
+
|
|
6318
|
+
# GPU checkbox (default True)
|
|
6319
|
+
self.GPU = QPushButton("GPU")
|
|
6320
|
+
self.GPU.setCheckable(True)
|
|
6321
|
+
self.GPU.setChecked(True)
|
|
6322
|
+
layout.addRow("Use GPU:", self.GPU)
|
|
6323
|
+
|
|
6324
|
+
self.down_factor = QLineEdit("")
|
|
6325
|
+
layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
|
|
6326
|
+
|
|
6327
|
+
# Add Run button
|
|
6328
|
+
run_button = QPushButton("Run Smart Label")
|
|
6329
|
+
run_button.clicked.connect(self.run_slabel)
|
|
6330
|
+
layout.addRow(run_button)
|
|
6331
|
+
|
|
6332
|
+
def run_slabel(self):
|
|
6333
|
+
|
|
6334
|
+
try:
|
|
6335
|
+
|
|
6336
|
+
accepted_source = self.mode_selector.currentIndex()
|
|
6337
|
+
accepted_target = self.target_selector.currentIndex()
|
|
6338
|
+
GPU = self.GPU.isChecked()
|
|
6339
|
+
|
|
6340
|
+
|
|
6341
|
+
if accepted_source == accepted_target:
|
|
6342
|
+
return
|
|
6343
|
+
|
|
6344
|
+
binary_array = self.parent().channel_data[accepted_target]
|
|
6345
|
+
|
|
6346
|
+
label_array = self.parent().channel_data[accepted_source]
|
|
6347
|
+
|
|
6348
|
+
down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
|
|
6349
|
+
|
|
6350
|
+
|
|
6351
|
+
try:
|
|
6352
|
+
|
|
6353
|
+
# Update both the display data and the network object
|
|
6354
|
+
binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor)
|
|
6355
|
+
|
|
6356
|
+
label_array = sdl.invert_array(label_array)
|
|
6357
|
+
|
|
6358
|
+
binary_array = binary_array * label_array
|
|
6359
|
+
|
|
6360
|
+
self.parent().load_channel(accepted_target, binary_array, True)
|
|
6361
|
+
|
|
6362
|
+
self.accept()
|
|
6363
|
+
|
|
6364
|
+
except Exception as e:
|
|
6365
|
+
QMessageBox.critical(
|
|
6366
|
+
self,
|
|
6367
|
+
"Error",
|
|
6368
|
+
f"Error running smart label: {str(e)}"
|
|
6369
|
+
)
|
|
6370
|
+
|
|
6371
|
+
except Exception as e:
|
|
6372
|
+
QMessageBox.critical(
|
|
6373
|
+
self,
|
|
6374
|
+
"Error",
|
|
6375
|
+
f"Error running smart label: {str(e)}"
|
|
6376
|
+
)
|
|
6377
|
+
|
|
6378
|
+
|
|
6264
6379
|
class ThresholdDialog(QDialog):
|
|
6265
6380
|
def __init__(self, parent=None):
|
|
6266
6381
|
super().__init__(parent)
|
|
@@ -6380,9 +6495,8 @@ class MachineWindow(QMainWindow):
|
|
|
6380
6495
|
act_channel = 1
|
|
6381
6496
|
|
|
6382
6497
|
|
|
6383
|
-
array1 = np.zeros_like(active_data)
|
|
6384
|
-
|
|
6385
|
-
array3 = np.zeros_like(active_data)
|
|
6498
|
+
array1 = np.zeros_like(active_data).astype(np.uint8)
|
|
6499
|
+
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
6386
6500
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
6387
6501
|
|
|
6388
6502
|
self.parent().load_channel(2, array1, True)
|
|
@@ -6460,7 +6574,7 @@ class MachineWindow(QMainWindow):
|
|
|
6460
6574
|
|
|
6461
6575
|
# Group 3: Training Options
|
|
6462
6576
|
training_group = QGroupBox("Training")
|
|
6463
|
-
training_layout =
|
|
6577
|
+
training_layout = QHBoxLayout()
|
|
6464
6578
|
train_quick = QPushButton("Train Quick Model")
|
|
6465
6579
|
train_quick.clicked.connect(lambda: self.train_model(speed=True))
|
|
6466
6580
|
train_detailed = QPushButton("Train More Detailed Model")
|
|
@@ -6471,11 +6585,13 @@ class MachineWindow(QMainWindow):
|
|
|
6471
6585
|
|
|
6472
6586
|
# Group 4: Segmentation Options
|
|
6473
6587
|
segmentation_group = QGroupBox("Segmentation")
|
|
6474
|
-
segmentation_layout =
|
|
6588
|
+
segmentation_layout = QHBoxLayout()
|
|
6475
6589
|
seg_button = QPushButton("Preview Segment")
|
|
6476
6590
|
self.seg_button = seg_button
|
|
6477
6591
|
seg_button.clicked.connect(self.start_segmentation)
|
|
6478
|
-
self.
|
|
6592
|
+
self.pause_button = QPushButton("▶/⏸️")
|
|
6593
|
+
self.pause_button.clicked.connect(self.pause)
|
|
6594
|
+
self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM)")
|
|
6479
6595
|
self.lock_button.setCheckable(True)
|
|
6480
6596
|
self.lock_button.setChecked(True)
|
|
6481
6597
|
self.lock_button.clicked.connect(self.toggle_lock)
|
|
@@ -6483,6 +6599,7 @@ class MachineWindow(QMainWindow):
|
|
|
6483
6599
|
full_button = QPushButton("Segment All")
|
|
6484
6600
|
full_button.clicked.connect(self.segment)
|
|
6485
6601
|
segmentation_layout.addWidget(seg_button)
|
|
6602
|
+
#segmentation_layout.addWidget(self.pause_button) # <--- for some reason the segmenter preview is still running even when killed, may be regenerating itself somewhere. May or may not actually try to resolve this because this feature isnt that necessary.
|
|
6486
6603
|
segmentation_layout.addWidget(self.lock_button)
|
|
6487
6604
|
segmentation_layout.addWidget(full_button)
|
|
6488
6605
|
segmentation_group.setLayout(segmentation_layout)
|
|
@@ -6507,6 +6624,27 @@ class MachineWindow(QMainWindow):
|
|
|
6507
6624
|
|
|
6508
6625
|
self.mem_lock = self.lock_button.isChecked()
|
|
6509
6626
|
|
|
6627
|
+
def pause(self):
|
|
6628
|
+
|
|
6629
|
+
if self.segmentation_worker is not None:
|
|
6630
|
+
try:
|
|
6631
|
+
print("Pausing segmenter")
|
|
6632
|
+
self.previewing = False
|
|
6633
|
+
self.segmentation_finished
|
|
6634
|
+
del self.segmentation_worker
|
|
6635
|
+
self.segmentation_worker = None
|
|
6636
|
+
except:
|
|
6637
|
+
pass
|
|
6638
|
+
|
|
6639
|
+
else:
|
|
6640
|
+
try:
|
|
6641
|
+
print("Restarting segmenter")
|
|
6642
|
+
self.previewing = True
|
|
6643
|
+
self.start_segmentation
|
|
6644
|
+
except:
|
|
6645
|
+
pass
|
|
6646
|
+
|
|
6647
|
+
|
|
6510
6648
|
|
|
6511
6649
|
def toggle_GPU(self):
|
|
6512
6650
|
|
|
@@ -6615,7 +6753,7 @@ class MachineWindow(QMainWindow):
|
|
|
6615
6753
|
else:
|
|
6616
6754
|
active_data = self.parent().channel_data[1]
|
|
6617
6755
|
|
|
6618
|
-
array3 = np.zeros_like(active_data)
|
|
6756
|
+
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
6619
6757
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
6620
6758
|
|
|
6621
6759
|
if not self.trained:
|
|
@@ -6801,7 +6939,7 @@ class MachineWindow(QMainWindow):
|
|
|
6801
6939
|
else:
|
|
6802
6940
|
active_data = self.parent().channel_data[1]
|
|
6803
6941
|
|
|
6804
|
-
array3 = np.zeros_like(active_data)
|
|
6942
|
+
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
6805
6943
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
6806
6944
|
|
|
6807
6945
|
print("Segmenting entire volume with model...")
|
|
@@ -7589,6 +7727,65 @@ class MaskDialog(QDialog):
|
|
|
7589
7727
|
print(f"Error masking: {e}")
|
|
7590
7728
|
|
|
7591
7729
|
|
|
7730
|
+
class TypeDialog(QDialog):
|
|
7731
|
+
|
|
7732
|
+
def __init__(self, parent=None):
|
|
7733
|
+
|
|
7734
|
+
super().__init__(parent)
|
|
7735
|
+
self.setWindowTitle("Active Channel dtype")
|
|
7736
|
+
self.setModal(True)
|
|
7737
|
+
|
|
7738
|
+
layout = QFormLayout(self)
|
|
7739
|
+
|
|
7740
|
+
self.active_chan = self.parent().active_channel
|
|
7741
|
+
|
|
7742
|
+
active_data = self.parent().channel_data[self.active_chan]
|
|
7743
|
+
|
|
7744
|
+
layout.addRow("Info:", QLabel(f"Active dtype (Channel {self.active_chan}): {active_data.dtype}"))
|
|
7745
|
+
|
|
7746
|
+
# Add mode selection dropdown
|
|
7747
|
+
self.mode_selector = QComboBox()
|
|
7748
|
+
self.mode_selector.addItems(["8bit int", "16bit int", "32bit int", "32bit float", "64bit float"])
|
|
7749
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
7750
|
+
layout.addRow("Change to?:", self.mode_selector)
|
|
7751
|
+
|
|
7752
|
+
# Add Run button
|
|
7753
|
+
run_button = QPushButton("Run")
|
|
7754
|
+
run_button.clicked.connect(lambda: self.run_type(active_data))
|
|
7755
|
+
layout.addRow(run_button)
|
|
7756
|
+
|
|
7757
|
+
def run_type(self, active_data):
|
|
7758
|
+
|
|
7759
|
+
mode = self.mode_selector.currentIndex()
|
|
7760
|
+
|
|
7761
|
+
if mode == 0:
|
|
7762
|
+
|
|
7763
|
+
active_data = active_data.astype(np.uint8)
|
|
7764
|
+
|
|
7765
|
+
elif mode == 1:
|
|
7766
|
+
|
|
7767
|
+
active_data = active_data.astype(np.uint16)
|
|
7768
|
+
|
|
7769
|
+
elif mode == 2:
|
|
7770
|
+
|
|
7771
|
+
active_data = active_data.astype(np.uint32)
|
|
7772
|
+
|
|
7773
|
+
elif mode == 3:
|
|
7774
|
+
|
|
7775
|
+
active_data = active_data.astype(np.float32)
|
|
7776
|
+
|
|
7777
|
+
elif mode == 4:
|
|
7778
|
+
|
|
7779
|
+
active_data = active_data.astype(np.float64)
|
|
7780
|
+
|
|
7781
|
+
self.parent().load_channel(self.active_chan, active_data, True)
|
|
7782
|
+
|
|
7783
|
+
|
|
7784
|
+
print(f"Channel {self.active_chan}) dtype now: {self.parent().channel_data[self.active_chan].dtype}")
|
|
7785
|
+
self.accept()
|
|
7786
|
+
|
|
7787
|
+
|
|
7788
|
+
|
|
7592
7789
|
|
|
7593
7790
|
class SkeletonizeDialog(QDialog):
|
|
7594
7791
|
def __init__(self, parent=None):
|
|
@@ -7601,6 +7798,12 @@ class SkeletonizeDialog(QDialog):
|
|
|
7601
7798
|
self.remove = QLineEdit("0")
|
|
7602
7799
|
layout.addRow("Remove Branches Pixel Length (int):", self.remove)
|
|
7603
7800
|
|
|
7801
|
+
# auto checkbox (default True)
|
|
7802
|
+
self.auto = QPushButton("Auto")
|
|
7803
|
+
self.auto.setCheckable(True)
|
|
7804
|
+
self.auto.setChecked(False)
|
|
7805
|
+
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
7806
|
+
|
|
7604
7807
|
# Add Run button
|
|
7605
7808
|
run_button = QPushButton("Run Skeletonize")
|
|
7606
7809
|
run_button.clicked.connect(self.run_skeletonize)
|
|
@@ -7614,11 +7817,17 @@ class SkeletonizeDialog(QDialog):
|
|
|
7614
7817
|
remove = int(self.remove.text()) if self.remove.text() else 0
|
|
7615
7818
|
except ValueError:
|
|
7616
7819
|
remove = 0
|
|
7820
|
+
|
|
7821
|
+
auto = self.auto.isChecked()
|
|
7617
7822
|
|
|
7618
7823
|
# Get the active channel data from parent
|
|
7619
7824
|
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
7620
7825
|
if active_data is None:
|
|
7621
7826
|
raise ValueError("No active image selected")
|
|
7827
|
+
|
|
7828
|
+
if auto:
|
|
7829
|
+
active_data = n3d.skeletonize(active_data)
|
|
7830
|
+
active_data = n3d.fill_holes_3d(active_data)
|
|
7622
7831
|
|
|
7623
7832
|
# Call dilate method with parameters
|
|
7624
7833
|
result = n3d.skeletonize(
|
|
@@ -7930,11 +8139,17 @@ class GenNodesDialog(QDialog):
|
|
|
7930
8139
|
self.branch_removal = QLineEdit("0")
|
|
7931
8140
|
layout.addRow("Skeleton Voxel Branch Length to Remove (int) (Compensates for spines off medial axis):", self.branch_removal)
|
|
7932
8141
|
|
|
8142
|
+
self.comp_dil = QLineEdit("0")
|
|
8143
|
+
layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
|
|
8144
|
+
|
|
7933
8145
|
self.max_vol = QLineEdit("0")
|
|
7934
8146
|
layout.addRow("Maximum Voxel Volume of Vertices to Retain (int - Compensates for skeleton looping - occurs before any node merging - the smallest objects are always 27 voxels):", self.max_vol)
|
|
7935
8147
|
|
|
7936
|
-
|
|
7937
|
-
|
|
8148
|
+
# auto checkbox (default True)
|
|
8149
|
+
self.auto = QPushButton("Auto")
|
|
8150
|
+
self.auto.setCheckable(True)
|
|
8151
|
+
self.auto.setChecked(False)
|
|
8152
|
+
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
7938
8153
|
|
|
7939
8154
|
if not down_factor:
|
|
7940
8155
|
down_factor = None
|
|
@@ -8012,6 +8227,13 @@ class GenNodesDialog(QDialog):
|
|
|
8012
8227
|
else:
|
|
8013
8228
|
order = 0
|
|
8014
8229
|
|
|
8230
|
+
auto = self.auto.isChecked()
|
|
8231
|
+
|
|
8232
|
+
|
|
8233
|
+
if auto:
|
|
8234
|
+
my_network.edges = n3d.skeletonize(my_network.edges)
|
|
8235
|
+
my_network.edges = n3d.fill_holes_3d(my_network.edges)
|
|
8236
|
+
|
|
8015
8237
|
|
|
8016
8238
|
result, skele = n3d.label_vertices(
|
|
8017
8239
|
my_network.edges,
|
|
@@ -8066,6 +8288,7 @@ class GenNodesDialog(QDialog):
|
|
|
8066
8288
|
)
|
|
8067
8289
|
|
|
8068
8290
|
|
|
8291
|
+
|
|
8069
8292
|
class BranchDialog(QDialog):
|
|
8070
8293
|
|
|
8071
8294
|
def __init__(self, parent=None):
|
|
@@ -8093,8 +8316,7 @@ class BranchDialog(QDialog):
|
|
|
8093
8316
|
self.fix.setChecked(False)
|
|
8094
8317
|
layout.addRow("Attempt to auto-correct branch labels:", self.fix)
|
|
8095
8318
|
|
|
8096
|
-
self.fix_val = QLineEdit()
|
|
8097
|
-
self.fix_val.setPlaceholderText("Empty = default value...")
|
|
8319
|
+
self.fix_val = QLineEdit('4')
|
|
8098
8320
|
layout.addRow("If checked above - Avg Degree of Nearby Branch Communities to Merge (Attempt to fix branch labeling - try 4 to 6 to start or leave empty):", self.fix_val)
|
|
8099
8321
|
|
|
8100
8322
|
self.down_factor = QLineEdit("0")
|
nettracer3d/network_analysis.py
CHANGED
|
@@ -1024,11 +1024,8 @@ def histogram(counts, y_vals, directory = None):
|
|
|
1024
1024
|
plt.ylabel('Avg Number of Neigbhoring Vertices')
|
|
1025
1025
|
|
|
1026
1026
|
try:
|
|
1027
|
-
|
|
1028
1027
|
if directory is not None:
|
|
1029
1028
|
plt.savefig(f'{directory}/radial_plot.png')
|
|
1030
|
-
else:
|
|
1031
|
-
plt.savefig('radial_plot.png')
|
|
1032
1029
|
except:
|
|
1033
1030
|
pass
|
|
1034
1031
|
|
|
@@ -1439,6 +1436,7 @@ def degree_distribution(G, directory = None):
|
|
|
1439
1436
|
|
|
1440
1437
|
def power_trendline(x, y, directory = None):
|
|
1441
1438
|
# Handle zeros in y for logarithmic transformations
|
|
1439
|
+
"""
|
|
1442
1440
|
y = np.array(y)
|
|
1443
1441
|
x = np.array(x)
|
|
1444
1442
|
y[y == 0] += 0.001
|
|
@@ -1460,6 +1458,8 @@ def power_trendline(x, y, directory = None):
|
|
|
1460
1458
|
ss_res = np.sum((y - y_pred) ** 2)
|
|
1461
1459
|
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
|
1462
1460
|
r2 = 1 - (ss_res / ss_tot)
|
|
1461
|
+
"""
|
|
1462
|
+
# ^ I commented out this power trendline stuff because I decided I no longer want it to do that so.
|
|
1463
1463
|
|
|
1464
1464
|
# Create a scatterplot
|
|
1465
1465
|
plt.scatter(x, y, label='Data')
|
|
@@ -1468,9 +1468,10 @@ def power_trendline(x, y, directory = None):
|
|
|
1468
1468
|
plt.title('Degree Distribution of Network')
|
|
1469
1469
|
|
|
1470
1470
|
# Plot the power trendline
|
|
1471
|
-
plt.plot(x_fit, y_fit, color='red', label=f'Power Trendline: $y = {a:.2f}x^{{{b:.2f}}}$')
|
|
1471
|
+
#plt.plot(x_fit, y_fit, color='red', label=f'Power Trendline: $y = {a:.2f}x^{{{b:.2f}}}$')
|
|
1472
1472
|
|
|
1473
1473
|
# Annotate the plot with the trendline equation and R-squared value
|
|
1474
|
+
"""
|
|
1474
1475
|
plt.text(
|
|
1475
1476
|
0.05, 0.95,
|
|
1476
1477
|
f'$y = {a:.2f}x^{{{b:.2f}}}$\n$R^2 = {r2:.2f}$',
|
|
@@ -1478,13 +1479,12 @@ def power_trendline(x, y, directory = None):
|
|
|
1478
1479
|
fontsize=12,
|
|
1479
1480
|
verticalalignment='top'
|
|
1480
1481
|
)
|
|
1482
|
+
"""
|
|
1481
1483
|
|
|
1482
1484
|
try:
|
|
1483
1485
|
|
|
1484
1486
|
if directory is not None:
|
|
1485
1487
|
plt.savefig(f'{directory}/degree_plot.png')
|
|
1486
|
-
else:
|
|
1487
|
-
plt.savefig('degree_plot.png')
|
|
1488
1488
|
except:
|
|
1489
1489
|
pass
|
|
1490
1490
|
|
nettracer3d/segmenter.py
CHANGED
|
@@ -1289,6 +1289,9 @@ class InteractiveSegmenter:
|
|
|
1289
1289
|
"""Segment volume using parallel processing of chunks with vectorized chunk creation"""
|
|
1290
1290
|
#Change the above chunk size to None to have it auto-compute largest chunks (not sure which is faster, 64 seems reasonable in test cases)
|
|
1291
1291
|
|
|
1292
|
+
self.realtimechunks = None # Presumably no longer need this.
|
|
1293
|
+
self.map_slice = None
|
|
1294
|
+
|
|
1292
1295
|
if self.mem_lock:
|
|
1293
1296
|
chunk_size = 32 #memory efficient chunk
|
|
1294
1297
|
|
|
@@ -1302,7 +1305,7 @@ class InteractiveSegmenter:
|
|
|
1302
1305
|
Returns:
|
|
1303
1306
|
List of chunks, where each chunk contains the coordinates for one z-slice or subchunk
|
|
1304
1307
|
"""
|
|
1305
|
-
MAX_CHUNK_SIZE =
|
|
1308
|
+
MAX_CHUNK_SIZE = 32768
|
|
1306
1309
|
chunks = []
|
|
1307
1310
|
|
|
1308
1311
|
for z in range(self.image_3d.shape[0]):
|
|
@@ -1690,6 +1693,7 @@ class InteractiveSegmenter:
|
|
|
1690
1693
|
def train_batch(self, foreground_array, speed = True, use_gpu = False, use_two = False, mem_lock = False):
|
|
1691
1694
|
"""Train directly on foreground and background arrays"""
|
|
1692
1695
|
|
|
1696
|
+
print("Training model...")
|
|
1693
1697
|
self.speed = speed
|
|
1694
1698
|
self.cur_gpu = use_gpu
|
|
1695
1699
|
if mem_lock != self.mem_lock:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
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
|
|
@@ -33,6 +33,7 @@ Provides-Extra: cuda12
|
|
|
33
33
|
Requires-Dist: cupy-cuda12x; extra == "cuda12"
|
|
34
34
|
Provides-Extra: cupy
|
|
35
35
|
Requires-Dist: cupy; extra == "cupy"
|
|
36
|
+
Dynamic: license-file
|
|
36
37
|
|
|
37
38
|
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:
|
|
38
39
|
|
|
@@ -44,8 +45,15 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
|
|
|
44
45
|
|
|
45
46
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
46
47
|
|
|
47
|
-
-- Version 0.6.
|
|
48
|
+
-- Version 0.6.4 updates --
|
|
48
49
|
|
|
49
|
-
1. Fixed bug with
|
|
50
|
+
1. Fixed bug with tabulated data in top right having a right click window corresponding to the bottom left.
|
|
50
51
|
|
|
51
|
-
2.
|
|
52
|
+
2. Fixed bug when converting communities to nodes that let the same community connect to itself in the new network.
|
|
53
|
+
|
|
54
|
+
3. Removed attempted trendline fitting from degree distribution
|
|
55
|
+
|
|
56
|
+
4. Added new feature to skeletonization (and corresponding branch labeler/gennodes)
|
|
57
|
+
Now you can have the program attempt to auto-correct 3D skeletonization loop artifacts through a method that just runs the 3d fill holes algo and then attempts to reskeletonize the output. This worked well in my own testing.
|
|
58
|
+
|
|
59
|
+
5. Other minor fixes/improvements
|
|
@@ -2,19 +2,19 @@ nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
nettracer3d/community_extractor.py,sha256=Zq8ZM595CTzeR6zLEZ4I6KvhkNfCPUReWvAKxTlaVfk,33495
|
|
3
3
|
nettracer3d/modularity.py,sha256=V1f3s_vGd8EuVz27mzq6ycIGr0BWIpH7c7NU4QjgAHU,30247
|
|
4
4
|
nettracer3d/morphology.py,sha256=yQ0GuieMVXOQpaohZlPnkEXEuCUjf8Fg352axyK8nbM,10755
|
|
5
|
-
nettracer3d/nettracer.py,sha256=
|
|
6
|
-
nettracer3d/nettracer_gui.py,sha256=
|
|
7
|
-
nettracer3d/network_analysis.py,sha256=
|
|
5
|
+
nettracer3d/nettracer.py,sha256=FRNmyFJM38k0sIY4yJJfgMUfSQ57E_2VQ6QWk08vFV0,208721
|
|
6
|
+
nettracer3d/nettracer_gui.py,sha256=tpNQha14Z1NbSb359qgvNOtvTH1mwkChdRGw0TGQG7A,384945
|
|
7
|
+
nettracer3d/network_analysis.py,sha256=q1q7lxtA3lebxitfC_jfiT9cnpYXJw4q0Oy2_-Aj8qE,48068
|
|
8
8
|
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
9
9
|
nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
|
|
10
10
|
nettracer3d/proximity.py,sha256=FnIiI_AzfXd22HwCIFIyQRZxKYJ8YscIDdPnIv-wsO4,10560
|
|
11
11
|
nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
|
|
12
|
-
nettracer3d/segmenter.py,sha256=
|
|
12
|
+
nettracer3d/segmenter.py,sha256=oKQEKQpo3o6cqfN6Z_IAgx8V-HXpegQNjfWFz3Bdu04,83449
|
|
13
13
|
nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
|
|
14
14
|
nettracer3d/smart_dilate.py,sha256=6m03KHtRMv0zfJ2aHc1Om4Fhh2abPy-IqDzRCmIEHCY,24588
|
|
15
|
-
nettracer3d-0.6.
|
|
16
|
-
nettracer3d-0.6.
|
|
17
|
-
nettracer3d-0.6.
|
|
18
|
-
nettracer3d-0.6.
|
|
19
|
-
nettracer3d-0.6.
|
|
20
|
-
nettracer3d-0.6.
|
|
15
|
+
nettracer3d-0.6.4.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
|
|
16
|
+
nettracer3d-0.6.4.dist-info/METADATA,sha256=noTR2x1vIJVQ6a3pkNbbD2kLsZGPaFvq7QG4gZuehJE,3646
|
|
17
|
+
nettracer3d-0.6.4.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
18
|
+
nettracer3d-0.6.4.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
19
|
+
nettracer3d-0.6.4.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
20
|
+
nettracer3d-0.6.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|