nettracer3d 0.9.5__py3-none-any.whl → 0.9.6__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/neighborhoods.py +17 -213
- nettracer3d/nettracer.py +128 -115
- nettracer3d/nettracer_gui.py +355 -85
- nettracer3d/proximity.py +91 -1
- {nettracer3d-0.9.5.dist-info → nettracer3d-0.9.6.dist-info}/METADATA +6 -4
- {nettracer3d-0.9.5.dist-info → nettracer3d-0.9.6.dist-info}/RECORD +10 -10
- {nettracer3d-0.9.5.dist-info → nettracer3d-0.9.6.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.5.dist-info → nettracer3d-0.9.6.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.5.dist-info → nettracer3d-0.9.6.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.5.dist-info → nettracer3d-0.9.6.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QG
|
|
|
5
5
|
QFormLayout, QLineEdit, QPushButton, QFileDialog,
|
|
6
6
|
QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
|
|
7
7
|
QMenu, QTabWidget, QGroupBox)
|
|
8
|
-
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent)
|
|
8
|
+
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent, QEventLoop)
|
|
9
9
|
import numpy as np
|
|
10
10
|
import time
|
|
11
11
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
@@ -1002,7 +1002,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1002
1002
|
pass
|
|
1003
1003
|
|
|
1004
1004
|
# Update display
|
|
1005
|
-
self.update_display(preserve_zoom=(current_xlim, current_ylim)
|
|
1005
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
1006
|
+
|
|
1007
|
+
if self.pan_mode:
|
|
1008
|
+
self.create_pan_background()
|
|
1009
|
+
current_xlim = self.ax.get_xlim()
|
|
1010
|
+
current_ylim = self.ax.get_ylim()
|
|
1011
|
+
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
1012
|
+
|
|
1013
|
+
|
|
1006
1014
|
|
|
1007
1015
|
def create_mini_overlay(self, node_indices = None, edge_indices = None):
|
|
1008
1016
|
|
|
@@ -2473,7 +2481,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2473
2481
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2474
2482
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2475
2483
|
|
|
2476
|
-
if self.high_button.isChecked() and self.machine_window is None:
|
|
2484
|
+
if self.high_button.isChecked() and self.machine_window is None and not self.preview:
|
|
2477
2485
|
if self.highlight_overlay is None and ((len(self.clicked_values['nodes']) + len(self.clicked_values['edges'])) > 0):
|
|
2478
2486
|
if self.needs_mini:
|
|
2479
2487
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
@@ -4237,9 +4245,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4237
4245
|
load_action.triggered.connect(lambda: self.load_misc('Edge Centroids'))
|
|
4238
4246
|
load_action = misc_menu.addAction("Load Node Communities")
|
|
4239
4247
|
load_action.triggered.connect(lambda: self.load_misc('Communities'))
|
|
4240
|
-
|
|
4248
|
+
node_identities = file_menu.addMenu('Images -> Node Identities')
|
|
4249
|
+
load_action = node_identities.addAction("Merge Labeled Images Into Nodes")
|
|
4241
4250
|
load_action.triggered.connect(lambda: self.load_misc('Merge Nodes'))
|
|
4242
|
-
load_action =
|
|
4251
|
+
load_action = node_identities.addAction("Assign Node Identities From Overlap With Other Images")
|
|
4243
4252
|
load_action.triggered.connect(self.show_merge_node_id_dialog)
|
|
4244
4253
|
|
|
4245
4254
|
|
|
@@ -4292,8 +4301,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4292
4301
|
id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
|
|
4293
4302
|
umap_action = overlay_menu.addAction("Centroid UMAP")
|
|
4294
4303
|
umap_action.triggered.connect(self.handle_centroid_umap)
|
|
4295
|
-
iden_umap_action = overlay_menu.addAction("Identity UMAP (If any nodes were assigned multiple identities)")
|
|
4296
|
-
iden_umap_action.triggered.connect(self.handle_iden_umap)
|
|
4297
4304
|
|
|
4298
4305
|
rand_menu = analysis_menu.addMenu("Randomize")
|
|
4299
4306
|
random_action = rand_menu.addAction("Generate Equivalent Random Network")
|
|
@@ -4363,7 +4370,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4363
4370
|
genvor_action = generate_menu.addAction("Generate Voronoi Diagram - goes in Overlay2")
|
|
4364
4371
|
genvor_action.triggered.connect(self.voronoi)
|
|
4365
4372
|
|
|
4366
|
-
modify_action = process_menu.addAction("Modify Network")
|
|
4373
|
+
modify_action = process_menu.addAction("Modify Network/Properties")
|
|
4367
4374
|
modify_action.triggered.connect(self.show_modify_dialog)
|
|
4368
4375
|
|
|
4369
4376
|
|
|
@@ -4711,6 +4718,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4711
4718
|
# Create new table
|
|
4712
4719
|
table = CustomTableView(self)
|
|
4713
4720
|
table.setModel(PandasModel(df))
|
|
4721
|
+
|
|
4722
|
+
try:
|
|
4723
|
+
first_column_name = table.model()._data.columns[0]
|
|
4724
|
+
table.sort_table(first_column_name, ascending=True)
|
|
4725
|
+
except:
|
|
4726
|
+
pass
|
|
4714
4727
|
|
|
4715
4728
|
# Add to tabbed widget
|
|
4716
4729
|
if title is None:
|
|
@@ -4718,17 +4731,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4718
4731
|
else:
|
|
4719
4732
|
self.tabbed_data.add_table(f"{title}", table)
|
|
4720
4733
|
|
|
4734
|
+
|
|
4735
|
+
|
|
4721
4736
|
# Adjust column widths to content
|
|
4722
4737
|
for column in range(table.model().columnCount(None)):
|
|
4723
4738
|
table.resizeColumnToContents(column)
|
|
4724
4739
|
|
|
4725
4740
|
except:
|
|
4726
|
-
|
|
4741
|
+
pass
|
|
4727
4742
|
|
|
4728
4743
|
def show_merge_node_id_dialog(self):
|
|
4729
4744
|
|
|
4730
|
-
|
|
4731
|
-
|
|
4745
|
+
if my_network.nodes is None:
|
|
4746
|
+
QMessageBox.critical(
|
|
4747
|
+
self,
|
|
4748
|
+
"Error",
|
|
4749
|
+
"Please load your segmented cells into 'Nodes' channel first"
|
|
4750
|
+
)
|
|
4751
|
+
return
|
|
4752
|
+
else:
|
|
4753
|
+
dialog = MergeNodeIdDialog(self)
|
|
4754
|
+
dialog.exec()
|
|
4732
4755
|
|
|
4733
4756
|
def show_gray_water_dialog(self):
|
|
4734
4757
|
"""Show the gray watershed parameter dialog."""
|
|
@@ -5095,29 +5118,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5095
5118
|
if sort == 'Node Identities':
|
|
5096
5119
|
my_network.load_node_identities(file_path = filename)
|
|
5097
5120
|
|
|
5098
|
-
"""
|
|
5099
|
-
first_value = list(my_network.node_identities.values())[0] # Check that there are not multiple IDs
|
|
5100
|
-
if isinstance(first_value, (list, tuple)):
|
|
5101
|
-
trump_value, ok = QInputDialog.getText(
|
|
5102
|
-
self,
|
|
5103
|
-
'Multiple IDs Detected',
|
|
5104
|
-
'The node identities appear to contain multiple ids per node in a list.\n'
|
|
5105
|
-
'If you desire one node ID to trump all others, enter it here.\n'
|
|
5106
|
-
'(Enter "-" to have the first IDs trump all others)\n'
|
|
5107
|
-
'(Enter "/" to have multi-ID nodes be split into many nodes sharing a centroid)\n'
|
|
5108
|
-
'(Close this window to continue with multi-ID nodes)'
|
|
5109
|
-
)
|
|
5110
|
-
if not ok or trump_value.strip() == '':
|
|
5111
|
-
trump_value = None
|
|
5112
|
-
elif trump_value.upper() == '-':
|
|
5113
|
-
trump_value = '-'
|
|
5114
|
-
elif trump_value.upper() == "/":
|
|
5115
|
-
trump_value = '/'
|
|
5116
|
-
my_network.node_identities = uncork(my_network.node_identities, trump_value)
|
|
5117
|
-
else:
|
|
5118
|
-
trump_value = None
|
|
5119
|
-
my_network.node_identities = uncork(my_network.node_identities, trump_value)
|
|
5120
|
-
"""
|
|
5121
5121
|
|
|
5122
5122
|
if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
|
|
5123
5123
|
try:
|
|
@@ -5166,6 +5166,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5166
5166
|
elif sort == 'Merge Nodes':
|
|
5167
5167
|
try:
|
|
5168
5168
|
|
|
5169
|
+
if my_network.nodes is None:
|
|
5170
|
+
QMessageBox.critical(
|
|
5171
|
+
self,
|
|
5172
|
+
"Error",
|
|
5173
|
+
"Please load your first set of nodes into the 'Nodes' channel first"
|
|
5174
|
+
)
|
|
5175
|
+
return
|
|
5176
|
+
|
|
5169
5177
|
if len(np.unique(my_network.nodes)) < 3:
|
|
5170
5178
|
self.show_label_dialog()
|
|
5171
5179
|
|
|
@@ -5179,6 +5187,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5179
5187
|
|
|
5180
5188
|
msg.exec()
|
|
5181
5189
|
|
|
5190
|
+
# Also if they want centroids:
|
|
5191
|
+
msg2 = QMessageBox()
|
|
5192
|
+
msg2.setWindowTitle("Selection Type")
|
|
5193
|
+
msg2.setText("Would you like to compute node centroids for each image prior to merging?")
|
|
5194
|
+
yes_button = msg2.addButton("Yes", QMessageBox.ButtonRole.AcceptRole)
|
|
5195
|
+
no_button = msg2.addButton("No", QMessageBox.ButtonRole.AcceptRole)
|
|
5196
|
+
msg2.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
|
5197
|
+
|
|
5198
|
+
msg2.exec()
|
|
5199
|
+
|
|
5200
|
+
if msg2.clickedButton() == yes_button:
|
|
5201
|
+
centroids = True
|
|
5202
|
+
else:
|
|
5203
|
+
centroids = False
|
|
5204
|
+
|
|
5182
5205
|
if msg.clickedButton() == tiff_button:
|
|
5183
5206
|
# Code for selecting TIFF files
|
|
5184
5207
|
filename, _ = QFileDialog.getOpenFileName(
|
|
@@ -5201,7 +5224,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5201
5224
|
if dialog.exec() == QFileDialog.DialogCode.Accepted:
|
|
5202
5225
|
selected_path = dialog.directory().absolutePath()
|
|
5203
5226
|
|
|
5204
|
-
my_network.merge_nodes(selected_path, root_id = self.node_name)
|
|
5227
|
+
my_network.merge_nodes(selected_path, root_id = self.node_name, centroids = centroids)
|
|
5205
5228
|
self.load_channel(0, my_network.nodes, True)
|
|
5206
5229
|
|
|
5207
5230
|
|
|
@@ -5210,8 +5233,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5210
5233
|
self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
5211
5234
|
except Exception as e:
|
|
5212
5235
|
print(f"Error loading node identity table: {e}")
|
|
5236
|
+
if centroids:
|
|
5237
|
+
self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
5238
|
+
|
|
5213
5239
|
|
|
5214
5240
|
except Exception as e:
|
|
5241
|
+
|
|
5215
5242
|
QMessageBox.critical(
|
|
5216
5243
|
self,
|
|
5217
5244
|
"Error Merging",
|
|
@@ -5729,6 +5756,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5729
5756
|
except:
|
|
5730
5757
|
pass
|
|
5731
5758
|
|
|
5759
|
+
if self.shape == self.channel_data[channel_index].shape:
|
|
5760
|
+
preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
|
|
5732
5761
|
self.shape = self.channel_data[channel_index].shape
|
|
5733
5762
|
if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
|
|
5734
5763
|
self.throttle = True
|
|
@@ -5820,7 +5849,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5820
5849
|
# Update display
|
|
5821
5850
|
self.update_display(preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim()))
|
|
5822
5851
|
|
|
5823
|
-
def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False, update = True):
|
|
5852
|
+
def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False, update = True, node_identities = False):
|
|
5824
5853
|
"""Method to flexibly reset certain fields to free up the RAM as desired"""
|
|
5825
5854
|
|
|
5826
5855
|
# Set scales first before any clearing operations
|
|
@@ -5840,6 +5869,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5840
5869
|
# Clear selection table
|
|
5841
5870
|
self.selection_table.setModel(PandasModel(empty_df))
|
|
5842
5871
|
|
|
5872
|
+
if node_identities:
|
|
5873
|
+
my_network.node_identities = None
|
|
5874
|
+
|
|
5843
5875
|
if nodes:
|
|
5844
5876
|
self.delete_channel(0, False, update = update)
|
|
5845
5877
|
|
|
@@ -5990,7 +6022,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5990
6022
|
# Now convert to real data
|
|
5991
6023
|
self.pm.convert_virtual_strokes_to_data()
|
|
5992
6024
|
self.current_slice = slice_value
|
|
5993
|
-
if self.
|
|
6025
|
+
if self.preview:
|
|
6026
|
+
self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6027
|
+
elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
|
|
5994
6028
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
5995
6029
|
if not self.hold_update:
|
|
5996
6030
|
self.update_display(preserve_zoom=view_settings)
|
|
@@ -6250,8 +6284,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6250
6284
|
vmin=0, vmax=1, extent=crop_extent)
|
|
6251
6285
|
|
|
6252
6286
|
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
6253
|
-
if self.preview and not called:
|
|
6254
|
-
|
|
6287
|
+
#if self.preview and not called:
|
|
6288
|
+
# self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6255
6289
|
|
|
6256
6290
|
# Overlay handling (optimized with cropping and downsampling)
|
|
6257
6291
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
@@ -6465,12 +6499,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6465
6499
|
|
|
6466
6500
|
my_network.centroid_umap()
|
|
6467
6501
|
|
|
6468
|
-
def handle_iden_umap(self):
|
|
6469
|
-
|
|
6470
|
-
if my_network.node_identities is None:
|
|
6471
|
-
return
|
|
6472
|
-
|
|
6473
|
-
my_network.identity_umap()
|
|
6474
6502
|
|
|
6475
6503
|
def closeEvent(self, event):
|
|
6476
6504
|
"""Override closeEvent to close all windows when main window closes"""
|
|
@@ -6487,6 +6515,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6487
6515
|
# Force quit the application
|
|
6488
6516
|
QCoreApplication.quit()
|
|
6489
6517
|
|
|
6518
|
+
exit()
|
|
6519
|
+
|
|
6490
6520
|
|
|
6491
6521
|
|
|
6492
6522
|
#TABLE RELATED:
|
|
@@ -6729,11 +6759,7 @@ class CustomTableView(QTableView):
|
|
|
6729
6759
|
self.resizeColumnToContents(col)
|
|
6730
6760
|
|
|
6731
6761
|
except Exception as e:
|
|
6732
|
-
|
|
6733
|
-
self,
|
|
6734
|
-
"Error",
|
|
6735
|
-
f"Error sorting table: {str(e)}"
|
|
6736
|
-
)
|
|
6762
|
+
pass
|
|
6737
6763
|
|
|
6738
6764
|
def save_table_as(self, file_type):
|
|
6739
6765
|
"""Save the table data as either CSV or Excel file."""
|
|
@@ -7253,8 +7279,13 @@ class PropertiesDialog(QDialog):
|
|
|
7253
7279
|
self.network.setChecked(self.check_checked(my_network.network))
|
|
7254
7280
|
layout.addRow("Network Status", self.network)
|
|
7255
7281
|
|
|
7282
|
+
self.node_identities = QPushButton("Node Identities")
|
|
7283
|
+
self.node_identities.setCheckable(True)
|
|
7284
|
+
self.node_identities.setChecked(self.check_checked(my_network.node_identities))
|
|
7285
|
+
layout.addRow("Identities Status", self.node_identities)
|
|
7286
|
+
|
|
7256
7287
|
# Add Run button
|
|
7257
|
-
run_button = QPushButton("Enter")
|
|
7288
|
+
run_button = QPushButton("Enter (Erases Unchecked Properties)")
|
|
7258
7289
|
run_button.clicked.connect(self.run_properties)
|
|
7259
7290
|
layout.addWidget(run_button)
|
|
7260
7291
|
|
|
@@ -7291,8 +7322,9 @@ class PropertiesDialog(QDialog):
|
|
|
7291
7322
|
id_overlay = not self.id_overlay.isChecked()
|
|
7292
7323
|
search_region = not self.search_region.isChecked()
|
|
7293
7324
|
network = not self.network.isChecked()
|
|
7325
|
+
node_identities = not self.node_identities.isChecked()
|
|
7294
7326
|
|
|
7295
|
-
self.parent().reset(nodes = nodes, edges = edges, search_region = search_region, network_overlay = network_overlay, id_overlay = id_overlay, network = network, xy_scale = xy_scale, z_scale = z_scale)
|
|
7327
|
+
self.parent().reset(nodes = nodes, edges = edges, search_region = search_region, network_overlay = network_overlay, id_overlay = id_overlay, network = network, xy_scale = xy_scale, z_scale = z_scale, node_identities = node_identities)
|
|
7296
7328
|
|
|
7297
7329
|
self.accept()
|
|
7298
7330
|
|
|
@@ -7720,78 +7752,252 @@ class ArbitraryDialog(QDialog):
|
|
|
7720
7752
|
except Exception as e:
|
|
7721
7753
|
QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
|
|
7722
7754
|
|
|
7723
|
-
class MergeNodeIdDialog(QDialog):
|
|
7724
7755
|
|
|
7756
|
+
class MergeNodeIdDialog(QDialog):
|
|
7725
7757
|
def __init__(self, parent=None):
|
|
7758
|
+
|
|
7726
7759
|
super().__init__(parent)
|
|
7760
|
+
|
|
7727
7761
|
self.setWindowTitle("Merging Node Identities From Folder Dialog.\nNote that you should prelabel or prewatershed your current node objects before doing this. (See Process -> Image) It does not label them for you.")
|
|
7728
7762
|
self.setModal(True)
|
|
7729
7763
|
|
|
7730
7764
|
layout = QFormLayout(self)
|
|
7731
|
-
|
|
7732
7765
|
self.search = QLineEdit("")
|
|
7733
7766
|
layout.addRow("Step-out distance (from current nodes image - ignore if you dilated them previously or don't want):", self.search)
|
|
7734
|
-
|
|
7735
7767
|
self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
|
|
7736
7768
|
layout.addRow("xy_scale:", self.xy_scale)
|
|
7737
|
-
|
|
7738
7769
|
self.z_scale = QLineEdit(f"{my_network.z_scale}")
|
|
7739
7770
|
layout.addRow("z_scale:", self.z_scale)
|
|
7771
|
+
self.mode_selector = QComboBox()
|
|
7772
|
+
self.mode_selector.addItems(["Auto-Binarize(Otsu)/Presegmented", "Manual (Interactive Thresholder)"])
|
|
7773
|
+
self.mode_selector.setCurrentIndex(1) # Default to Mode 1
|
|
7774
|
+
layout.addRow("Binarization Strategy:", self.mode_selector)
|
|
7740
7775
|
|
|
7741
|
-
|
|
7742
|
-
self.
|
|
7776
|
+
self.umap = QPushButton("If using manual threshold: Also generate identity UMAP?")
|
|
7777
|
+
self.umap.setCheckable(True)
|
|
7778
|
+
self.umap.setChecked(True)
|
|
7779
|
+
layout.addWidget(self.umap)
|
|
7780
|
+
|
|
7781
|
+
self.include = QPushButton("Include When a Node is Negative for an ID?")
|
|
7743
7782
|
self.include.setCheckable(True)
|
|
7744
|
-
self.include.setChecked(
|
|
7783
|
+
self.include.setChecked(False)
|
|
7745
7784
|
layout.addWidget(self.include)
|
|
7746
|
-
|
|
7747
|
-
# Add Run button
|
|
7785
|
+
|
|
7748
7786
|
run_button = QPushButton("Get Directory")
|
|
7749
7787
|
run_button.clicked.connect(self.run)
|
|
7750
7788
|
layout.addWidget(run_button)
|
|
7751
7789
|
|
|
7752
|
-
def
|
|
7790
|
+
def wait_for_threshold_processing(self):
|
|
7791
|
+
"""
|
|
7792
|
+
Opens ThresholdWindow and waits for user to process the image.
|
|
7793
|
+
Returns True if completed, False if cancelled.
|
|
7794
|
+
The thresholded image will be available in the main window after completion.
|
|
7795
|
+
"""
|
|
7796
|
+
# Create event loop to wait for user
|
|
7797
|
+
loop = QEventLoop()
|
|
7798
|
+
result = {'completed': False}
|
|
7799
|
+
|
|
7800
|
+
# Create the threshold window
|
|
7801
|
+
thresh_window = ThresholdWindow(self.parent(), 4)
|
|
7802
|
+
|
|
7803
|
+
# Connect signals
|
|
7804
|
+
def on_processing_complete():
|
|
7805
|
+
result['completed'] = True
|
|
7806
|
+
loop.quit()
|
|
7807
|
+
|
|
7808
|
+
def on_processing_cancelled():
|
|
7809
|
+
result['completed'] = False
|
|
7810
|
+
loop.quit()
|
|
7811
|
+
|
|
7812
|
+
thresh_window.processing_complete.connect(on_processing_complete)
|
|
7813
|
+
thresh_window.processing_cancelled.connect(on_processing_cancelled)
|
|
7814
|
+
|
|
7815
|
+
# Show window and wait
|
|
7816
|
+
thresh_window.show()
|
|
7817
|
+
thresh_window.raise_()
|
|
7818
|
+
thresh_window.activateWindow()
|
|
7819
|
+
|
|
7820
|
+
# Block until user clicks "Apply Threshold & Continue" or "Cancel"
|
|
7821
|
+
loop.exec()
|
|
7822
|
+
|
|
7823
|
+
# Clean up
|
|
7824
|
+
thresh_window.deleteLater()
|
|
7825
|
+
|
|
7826
|
+
return result['completed']
|
|
7753
7827
|
|
|
7828
|
+
def run(self):
|
|
7754
7829
|
try:
|
|
7755
7830
|
|
|
7756
7831
|
search = float(self.search.text()) if self.search.text().strip() else 0
|
|
7757
7832
|
xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
|
|
7758
7833
|
z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
7834
|
data = self.parent().channel_data[0]
|
|
7762
7835
|
include = self.include.isChecked()
|
|
7763
|
-
|
|
7836
|
+
umap = self.umap.isChecked()
|
|
7837
|
+
|
|
7764
7838
|
if data is None:
|
|
7765
7839
|
return
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7840
|
+
|
|
7769
7841
|
dialog = QFileDialog(self)
|
|
7770
7842
|
dialog.setOption(QFileDialog.Option.DontUseNativeDialog)
|
|
7771
7843
|
dialog.setOption(QFileDialog.Option.ReadOnly)
|
|
7772
7844
|
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
|
7773
7845
|
dialog.setViewMode(QFileDialog.ViewMode.Detail)
|
|
7774
|
-
|
|
7846
|
+
|
|
7775
7847
|
if dialog.exec() == QFileDialog.DialogCode.Accepted:
|
|
7776
7848
|
selected_path = dialog.directory().absolutePath()
|
|
7777
|
-
|
|
7849
|
+
else:
|
|
7850
|
+
return # User cancelled directory selection
|
|
7851
|
+
|
|
7778
7852
|
if search > 0:
|
|
7779
|
-
data = sdl.smart_dilate(data, 1, 1, GPU
|
|
7853
|
+
data = sdl.smart_dilate(data, 1, 1, GPU=False, fast_dil=False,
|
|
7854
|
+
use_dt_dil_amount=search, xy_scale=xy_scale, z_scale=z_scale)
|
|
7855
|
+
|
|
7856
|
+
# Check if manual mode is selected
|
|
7857
|
+
if self.mode_selector.currentIndex() == 1: # Manual mode
|
|
7780
7858
|
|
|
7781
|
-
|
|
7859
|
+
if my_network.node_identities is None: # Prepare modular dict
|
|
7782
7860
|
|
|
7783
|
-
|
|
7861
|
+
my_network.node_identities = {}
|
|
7784
7862
|
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7863
|
+
nodes = list(np.unique(data))
|
|
7864
|
+
if 0 in nodes:
|
|
7865
|
+
del nodes[0]
|
|
7866
|
+
for node in nodes:
|
|
7867
|
+
|
|
7868
|
+
my_network.node_identities[node] = [] # Assign to lists at first
|
|
7869
|
+
else:
|
|
7870
|
+
for node, iden in my_network.node_identities.items():
|
|
7871
|
+
try:
|
|
7872
|
+
my_network.node_identities[node] = ast.literal_eval(iden)
|
|
7873
|
+
except:
|
|
7874
|
+
my_network.node_identities[node] = [iden]
|
|
7875
|
+
|
|
7876
|
+
id_dicts = my_network.get_merge_node_dictionaries(selected_path, data)
|
|
7877
|
+
|
|
7878
|
+
# For loop example - get threshold for multiple images/data
|
|
7879
|
+
results = []
|
|
7880
|
+
|
|
7881
|
+
img_list = n3d.directory_info(selected_path)
|
|
7882
|
+
data_backup = copy.deepcopy(data)
|
|
7883
|
+
self.parent().load_channel(0, data, data = True)
|
|
7884
|
+
self.hide()
|
|
7885
|
+
self.parent().highlight_overlay = None
|
|
7886
|
+
|
|
7887
|
+
good_list = []
|
|
7888
|
+
|
|
7889
|
+
for i, img in enumerate(img_list):
|
|
7890
|
+
|
|
7891
|
+
if img.endswith('.tiff') or img.endswith('.tif'):
|
|
7892
|
+
|
|
7893
|
+
print(f"Please threshold {img}")
|
|
7894
|
+
|
|
7895
|
+
|
|
7896
|
+
mask = tifffile.imread(f'{selected_path}/{img}')
|
|
7897
|
+
self.parent().load_channel(2, mask, data = True)
|
|
7898
|
+
|
|
7899
|
+
# Wait for user to threshold this data
|
|
7900
|
+
self.parent().special_dict = id_dicts[i]
|
|
7901
|
+
processing_completed = self.wait_for_threshold_processing()
|
|
7902
|
+
|
|
7903
|
+
if not processing_completed:
|
|
7904
|
+
# User cancelled, ask if they want to continue
|
|
7905
|
+
reply = QMessageBox.question(self, 'Continue?',
|
|
7906
|
+
f'Threshold cancelled for item {i+1}. Continue with remaining items?',
|
|
7907
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
7908
|
+
if reply == QMessageBox.StandardButton.No:
|
|
7909
|
+
break
|
|
7910
|
+
continue
|
|
7911
|
+
|
|
7912
|
+
# At this point, the thresholded image is in the main window's memory
|
|
7913
|
+
# Get the processed/thresholded data from wherever ThresholdWindow stored it
|
|
7914
|
+
thresholded_vals = list(np.unique(self.parent().channel_data[0]))
|
|
7915
|
+
if 0 in thresholded_vals:
|
|
7916
|
+
del thresholded_vals[0]
|
|
7917
|
+
|
|
7918
|
+
if img.endswith('.tiff'):
|
|
7919
|
+
base_name = img[:-5]
|
|
7920
|
+
elif img.endswith('.tif'):
|
|
7921
|
+
base_name = img[:-4]
|
|
7922
|
+
else:
|
|
7923
|
+
base_name = img
|
|
7924
|
+
|
|
7925
|
+
assigned = {}
|
|
7926
|
+
|
|
7927
|
+
for node in my_network.node_identities.keys():
|
|
7928
|
+
|
|
7929
|
+
try:
|
|
7930
|
+
|
|
7931
|
+
if int(node) in thresholded_vals:
|
|
7932
|
+
|
|
7933
|
+
my_network.node_identities[node].append(f'{base_name}+')
|
|
7934
|
+
|
|
7935
|
+
elif include:
|
|
7936
|
+
|
|
7937
|
+
my_network.node_identities[node].append(f'{base_name}-')
|
|
7938
|
+
|
|
7939
|
+
except:
|
|
7940
|
+
pass
|
|
7941
|
+
|
|
7942
|
+
# Process the thresholded data
|
|
7943
|
+
self.parent().highlight_overlay = None
|
|
7944
|
+
self.parent().load_channel(0, data_backup, data = True)
|
|
7945
|
+
good_list.append(base_name)
|
|
7946
|
+
|
|
7947
|
+
modify_dict = copy.deepcopy(my_network.node_identities)
|
|
7948
|
+
|
|
7949
|
+
for node, iden in my_network.node_identities.items():
|
|
7950
|
+
|
|
7951
|
+
try:
|
|
7952
|
+
|
|
7953
|
+
if len(iden) == 1:
|
|
7954
|
+
|
|
7955
|
+
modify_dict[node] = str(iden[0]) # Singleton lists become bare strings
|
|
7956
|
+
elif len(iden) == 0:
|
|
7957
|
+
del modify_dict[node]
|
|
7958
|
+
else:
|
|
7959
|
+
modify_dict[node] = str(iden) # We hold multi element lists as strings for compatibility
|
|
7960
|
+
|
|
7961
|
+
except:
|
|
7962
|
+
pass
|
|
7963
|
+
|
|
7964
|
+
my_network.node_identities = modify_dict
|
|
7965
|
+
|
|
7966
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
|
|
7967
|
+
|
|
7968
|
+
all_keys = id_dicts[0].keys()
|
|
7969
|
+
result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
|
|
7970
|
+
|
|
7971
|
+
|
|
7972
|
+
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity')
|
|
7973
|
+
if umap:
|
|
7974
|
+
my_network.identity_umap(result)
|
|
7790
7975
|
|
|
7791
|
-
|
|
7976
|
+
|
|
7977
|
+
QMessageBox.information(
|
|
7978
|
+
self,
|
|
7979
|
+
"Success",
|
|
7980
|
+
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
|
|
7981
|
+
)
|
|
7982
|
+
|
|
7983
|
+
self.accept()
|
|
7984
|
+
else:
|
|
7985
|
+
my_network.merge_node_ids(selected_path, data, include)
|
|
7986
|
+
|
|
7987
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
|
|
7988
|
+
|
|
7989
|
+
QMessageBox.information(
|
|
7990
|
+
self,
|
|
7991
|
+
"Success",
|
|
7992
|
+
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
|
|
7993
|
+
)
|
|
7994
|
+
|
|
7995
|
+
self.accept()
|
|
7792
7996
|
|
|
7793
7997
|
except Exception as e:
|
|
7794
|
-
|
|
7998
|
+
import traceback
|
|
7999
|
+
print(traceback.format_exc())
|
|
8000
|
+
#print(f"Error: {e}")
|
|
7795
8001
|
|
|
7796
8002
|
|
|
7797
8003
|
class Show3dDialog(QDialog):
|
|
@@ -11172,11 +11378,15 @@ class SegmentationWorker(QThread):
|
|
|
11172
11378
|
|
|
11173
11379
|
|
|
11174
11380
|
class ThresholdWindow(QMainWindow):
|
|
11381
|
+
processing_complete = pyqtSignal() # Emitted when user finishes and images are modified
|
|
11382
|
+
processing_cancelled = pyqtSignal() # Emitted when user cancels
|
|
11383
|
+
|
|
11175
11384
|
def __init__(self, parent=None, accepted_mode=0):
|
|
11176
11385
|
super().__init__(parent)
|
|
11177
11386
|
self.setWindowTitle("Threshold")
|
|
11178
11387
|
|
|
11179
11388
|
self.accepted_mode = accepted_mode
|
|
11389
|
+
self.preview = True
|
|
11180
11390
|
|
|
11181
11391
|
# Create central widget and layout
|
|
11182
11392
|
central_widget = QWidget()
|
|
@@ -11208,6 +11418,10 @@ class ThresholdWindow(QMainWindow):
|
|
|
11208
11418
|
self.histo_list = list(self.parent().degree_dict.values())
|
|
11209
11419
|
self.bounds = False
|
|
11210
11420
|
self.parent().bounds = False
|
|
11421
|
+
elif accepted_mode == 4:
|
|
11422
|
+
self.histo_list = list(self.parent().special_dict.values())
|
|
11423
|
+
self.bounds = False
|
|
11424
|
+
self.parent().bounds = False
|
|
11211
11425
|
|
|
11212
11426
|
elif accepted_mode == 0:
|
|
11213
11427
|
targ_shape = self.parent().channel_data[self.parent().active_channel].shape
|
|
@@ -11279,16 +11493,39 @@ class ThresholdWindow(QMainWindow):
|
|
|
11279
11493
|
self.preview.clicked.connect(self.preview_mode)
|
|
11280
11494
|
form_layout.addRow("Show Preview:", self.preview)
|
|
11281
11495
|
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11496
|
+
button_layout = QHBoxLayout()
|
|
11497
|
+
|
|
11498
|
+
|
|
11499
|
+
# Keep your existing Apply Threshold button, but modify its behavior
|
|
11500
|
+
run_button = QPushButton("Apply Threshold/Continue")
|
|
11501
|
+
run_button.clicked.connect(self.apply_and_continue) # New method
|
|
11502
|
+
button_layout.addWidget(run_button)
|
|
11285
11503
|
|
|
11286
|
-
|
|
11504
|
+
# Add Cancel button for external dialog use
|
|
11505
|
+
cancel_button = QPushButton("Cancel/Skip")
|
|
11506
|
+
cancel_button.clicked.connect(self.cancel_processing)
|
|
11507
|
+
button_layout.addWidget(cancel_button)
|
|
11287
11508
|
|
|
11509
|
+
form_layout.addRow(button_layout)
|
|
11510
|
+
layout.addLayout(form_layout)
|
|
11511
|
+
|
|
11288
11512
|
# Set a reasonable default size
|
|
11289
11513
|
self.setMinimumWidth(400)
|
|
11290
11514
|
self.setMinimumHeight(400)
|
|
11291
11515
|
|
|
11516
|
+
def apply_and_continue(self):
|
|
11517
|
+
"""Apply threshold, modify main window images, then signal completion"""
|
|
11518
|
+
self.thresh() # This should modify the main window images
|
|
11519
|
+
|
|
11520
|
+
# Signal that processing is complete
|
|
11521
|
+
self.processing_complete.emit()
|
|
11522
|
+
self.close()
|
|
11523
|
+
|
|
11524
|
+
def cancel_processing(self):
|
|
11525
|
+
"""Cancel without applying changes"""
|
|
11526
|
+
self.processing_cancelled.emit()
|
|
11527
|
+
self.close()
|
|
11528
|
+
|
|
11292
11529
|
def closeEvent(self, event):
|
|
11293
11530
|
self.parent().preview = False
|
|
11294
11531
|
self.parent().targs = None
|
|
@@ -11340,6 +11577,10 @@ class ThresholdWindow(QMainWindow):
|
|
|
11340
11577
|
for node, vol in self.parent().degree_dict.items():
|
|
11341
11578
|
if min_val <= vol <= max_val:
|
|
11342
11579
|
output.append(node)
|
|
11580
|
+
elif self.accepted_mode == 4:
|
|
11581
|
+
for node, vol in self.parent().special_dict.items():
|
|
11582
|
+
if min_val <= vol <= max_val:
|
|
11583
|
+
output.append(node)
|
|
11343
11584
|
return output
|
|
11344
11585
|
|
|
11345
11586
|
def get_values_in_range(self, lst, min_val, max_val):
|
|
@@ -11357,11 +11598,18 @@ class ThresholdWindow(QMainWindow):
|
|
|
11357
11598
|
for item in self.parent().degree_dict:
|
|
11358
11599
|
if self.parent().degree_dict[item] in values:
|
|
11359
11600
|
output.append(item)
|
|
11601
|
+
elif self.accepted_mode == 4:
|
|
11602
|
+
for item in self.parent().special_dict:
|
|
11603
|
+
if self.parent().special_dict[item] in values:
|
|
11604
|
+
output.append(item)
|
|
11605
|
+
|
|
11360
11606
|
return output
|
|
11361
11607
|
|
|
11362
11608
|
|
|
11363
11609
|
def min_value_changed(self):
|
|
11364
11610
|
try:
|
|
11611
|
+
if not self.preview.isChecked():
|
|
11612
|
+
self.preview.click()
|
|
11365
11613
|
text = self.min.text()
|
|
11366
11614
|
if not text: # If empty, ignore
|
|
11367
11615
|
return
|
|
@@ -11409,6 +11657,8 @@ class ThresholdWindow(QMainWindow):
|
|
|
11409
11657
|
|
|
11410
11658
|
def max_value_changed(self):
|
|
11411
11659
|
try:
|
|
11660
|
+
if not self.preview.isChecked():
|
|
11661
|
+
self.preview.click()
|
|
11412
11662
|
text = self.max.text()
|
|
11413
11663
|
if not text: # If empty, ignore
|
|
11414
11664
|
return
|
|
@@ -11532,7 +11782,6 @@ class ThresholdWindow(QMainWindow):
|
|
|
11532
11782
|
f"Error running threshold: {str(e)}"
|
|
11533
11783
|
)
|
|
11534
11784
|
|
|
11535
|
-
|
|
11536
11785
|
class SmartDilateDialog(QDialog):
|
|
11537
11786
|
def __init__(self, parent, params):
|
|
11538
11787
|
super().__init__(parent)
|
|
@@ -13191,6 +13440,12 @@ class ModifyDialog(QDialog):
|
|
|
13191
13440
|
self.revid.setChecked(False)
|
|
13192
13441
|
layout.addRow("Remove Unassigned IDs from Centroid List?:", self.revid)
|
|
13193
13442
|
|
|
13443
|
+
self.revdupeid = QPushButton("Make Singleton IDs")
|
|
13444
|
+
self.revdupeid.setCheckable(True)
|
|
13445
|
+
self.revdupeid.setChecked(False)
|
|
13446
|
+
layout.addRow("Force Any Multiple IDs to Pick a Random Single ID?:", self.revdupeid)
|
|
13447
|
+
|
|
13448
|
+
|
|
13194
13449
|
self.remove = QPushButton("Remove Missing")
|
|
13195
13450
|
self.remove.setCheckable(True)
|
|
13196
13451
|
self.remove.setChecked(False)
|
|
@@ -13269,6 +13524,7 @@ class ModifyDialog(QDialog):
|
|
|
13269
13524
|
try:
|
|
13270
13525
|
|
|
13271
13526
|
revid = self.revid.isChecked()
|
|
13527
|
+
revdupeid = self.revdupeid.isChecked()
|
|
13272
13528
|
trunk = self.trunk.isChecked()
|
|
13273
13529
|
if not trunk:
|
|
13274
13530
|
trunknode = self.trunknode.isChecked()
|
|
@@ -13293,6 +13549,20 @@ class ModifyDialog(QDialog):
|
|
|
13293
13549
|
except:
|
|
13294
13550
|
pass
|
|
13295
13551
|
|
|
13552
|
+
if revdupeid:
|
|
13553
|
+
try:
|
|
13554
|
+
for node, iden in my_network.node_identities.items():
|
|
13555
|
+
try:
|
|
13556
|
+
import ast
|
|
13557
|
+
import random
|
|
13558
|
+
iden = ast.literal_eval(iden)
|
|
13559
|
+
my_network.node_identities[node] = random.choice(iden)
|
|
13560
|
+
except:
|
|
13561
|
+
pass
|
|
13562
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
13563
|
+
except:
|
|
13564
|
+
pass
|
|
13565
|
+
|
|
13296
13566
|
|
|
13297
13567
|
if remove:
|
|
13298
13568
|
my_network.purge_properties()
|