celldetective 1.5.0b8__py3-none-any.whl → 1.5.0b9__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.
- celldetective/_version.py +1 -1
- celldetective/gui/base/list_widget.py +1 -1
- celldetective/gui/generic_signal_plot.py +14 -14
- celldetective/gui/gui_utils.py +27 -6
- celldetective/gui/plot_signals_ui.py +32 -15
- celldetective/gui/survival_ui.py +39 -11
- celldetective/gui/tableUI.py +69 -148
- celldetective/gui/thresholds_gui.py +45 -5
- celldetective/gui/viewers/spot_detection_viewer.py +136 -27
- {celldetective-1.5.0b8.dist-info → celldetective-1.5.0b9.dist-info}/METADATA +1 -1
- {celldetective-1.5.0b8.dist-info → celldetective-1.5.0b9.dist-info}/RECORD +17 -16
- tests/gui/test_spot_detection_viewer.py +187 -0
- tests/test_signals.py +131 -112
- {celldetective-1.5.0b8.dist-info → celldetective-1.5.0b9.dist-info}/WHEEL +0 -0
- {celldetective-1.5.0b8.dist-info → celldetective-1.5.0b9.dist-info}/entry_points.txt +0 -0
- {celldetective-1.5.0b8.dist-info → celldetective-1.5.0b9.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.5.0b8.dist-info → celldetective-1.5.0b9.dist-info}/top_level.txt +0 -0
celldetective/gui/tableUI.py
CHANGED
|
@@ -21,8 +21,13 @@ from celldetective.gui.gui_utils import (
|
|
|
21
21
|
)
|
|
22
22
|
from celldetective.gui.base.figure_canvas import FigureCanvas
|
|
23
23
|
from celldetective.gui.base.utils import center_window
|
|
24
|
-
from celldetective.gui.table_ops._maths import
|
|
25
|
-
|
|
24
|
+
from celldetective.gui.table_ops._maths import (
|
|
25
|
+
DifferentiateColWidget,
|
|
26
|
+
OperationOnColsWidget,
|
|
27
|
+
CalibrateColWidget,
|
|
28
|
+
AbsColWidget,
|
|
29
|
+
LogColWidget,
|
|
30
|
+
)
|
|
26
31
|
from celldetective.gui.table_ops._merge_one_hot import MergeOneHotWidget
|
|
27
32
|
from celldetective.gui.table_ops._query_table import QueryWidget
|
|
28
33
|
from celldetective.gui.table_ops._rename_col import RenameColWidget
|
|
@@ -55,7 +60,7 @@ class PivotTableUI(CelldetectiveWidget):
|
|
|
55
60
|
self.mode = mode
|
|
56
61
|
|
|
57
62
|
self.setWindowTitle(title)
|
|
58
|
-
|
|
63
|
+
logger.debug(f"Pivot table to show: {self.data.shape}")
|
|
59
64
|
|
|
60
65
|
self.table = QTableView(self)
|
|
61
66
|
|
|
@@ -219,9 +224,35 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
219
224
|
|
|
220
225
|
try:
|
|
221
226
|
self.fig.tight_layout()
|
|
222
|
-
except:
|
|
227
|
+
except AttributeError:
|
|
228
|
+
# fig not yet created
|
|
223
229
|
pass
|
|
224
230
|
|
|
231
|
+
def _get_selected_columns(self, max_cols=None):
|
|
232
|
+
"""
|
|
233
|
+
Get selected column names from the table view.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
max_cols : int, optional
|
|
238
|
+
Maximum number of columns to return. Returns all if None.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
list
|
|
243
|
+
List of selected column names.
|
|
244
|
+
"""
|
|
245
|
+
x = self.table_view.selectedIndexes()
|
|
246
|
+
col_idx = np.unique(np.array([l.column() for l in x]))
|
|
247
|
+
cols = np.array(list(self.data.columns))
|
|
248
|
+
result = []
|
|
249
|
+
if len(col_idx) > 0:
|
|
250
|
+
for i in col_idx:
|
|
251
|
+
result.append(str(cols[i]))
|
|
252
|
+
if max_cols is not None:
|
|
253
|
+
return result[:max_cols]
|
|
254
|
+
return result
|
|
255
|
+
|
|
225
256
|
def _create_actions(self):
|
|
226
257
|
|
|
227
258
|
self.save_as = QAction("&Save as...", self)
|
|
@@ -540,8 +571,8 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
540
571
|
self.renameWidget.show()
|
|
541
572
|
|
|
542
573
|
def save_as_csv_inplace_per_pos(self):
|
|
543
|
-
|
|
544
|
-
|
|
574
|
+
"""Save each position's table in its respective folder."""
|
|
575
|
+
logger.info("Saving each table in its respective position folder...")
|
|
545
576
|
for pos, pos_group in self.data.groupby(["position"]):
|
|
546
577
|
invalid_cols = [
|
|
547
578
|
c for c in list(pos_group.columns) if c.startswith("Unnamed")
|
|
@@ -555,26 +586,12 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
555
586
|
),
|
|
556
587
|
index=False,
|
|
557
588
|
)
|
|
558
|
-
|
|
589
|
+
logger.info("Done saving tables.")
|
|
559
590
|
|
|
560
591
|
def divide_signals(self):
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
565
|
-
cols = np.array(list(self.data.columns))
|
|
566
|
-
if len(col_idx) > 0:
|
|
567
|
-
selected_col1 = str(cols[col_idx[0]])
|
|
568
|
-
if len(col_idx) > 1:
|
|
569
|
-
selected_col2 = str(cols[col_idx[1]])
|
|
570
|
-
else:
|
|
571
|
-
selected_col2 = None
|
|
572
|
-
else:
|
|
573
|
-
selected_col1 = None
|
|
574
|
-
selected_col2 = None
|
|
575
|
-
else:
|
|
576
|
-
selected_col1 = None
|
|
577
|
-
selected_col2 = None
|
|
592
|
+
selected = self._get_selected_columns(max_cols=2)
|
|
593
|
+
selected_col1 = selected[0] if len(selected) > 0 else None
|
|
594
|
+
selected_col2 = selected[1] if len(selected) > 1 else None
|
|
578
595
|
|
|
579
596
|
self.divWidget = OperationOnColsWidget(
|
|
580
597
|
self, column1=selected_col1, column2=selected_col2, operation="divide"
|
|
@@ -582,23 +599,9 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
582
599
|
self.divWidget.show()
|
|
583
600
|
|
|
584
601
|
def multiply_signals(self):
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
589
|
-
cols = np.array(list(self.data.columns))
|
|
590
|
-
if len(col_idx) > 0:
|
|
591
|
-
selected_col1 = str(cols[col_idx[0]])
|
|
592
|
-
if len(col_idx) > 1:
|
|
593
|
-
selected_col2 = str(cols[col_idx[1]])
|
|
594
|
-
else:
|
|
595
|
-
selected_col2 = None
|
|
596
|
-
else:
|
|
597
|
-
selected_col1 = None
|
|
598
|
-
selected_col2 = None
|
|
599
|
-
else:
|
|
600
|
-
selected_col1 = None
|
|
601
|
-
selected_col2 = None
|
|
602
|
+
selected = self._get_selected_columns(max_cols=2)
|
|
603
|
+
selected_col1 = selected[0] if len(selected) > 0 else None
|
|
604
|
+
selected_col2 = selected[1] if len(selected) > 1 else None
|
|
602
605
|
|
|
603
606
|
self.mulWidget = OperationOnColsWidget(
|
|
604
607
|
self, column1=selected_col1, column2=selected_col2, operation="multiply"
|
|
@@ -606,23 +609,9 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
606
609
|
self.mulWidget.show()
|
|
607
610
|
|
|
608
611
|
def add_signals(self):
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
613
|
-
cols = np.array(list(self.data.columns))
|
|
614
|
-
if len(col_idx) > 0:
|
|
615
|
-
selected_col1 = str(cols[col_idx[0]])
|
|
616
|
-
if len(col_idx) > 1:
|
|
617
|
-
selected_col2 = str(cols[col_idx[1]])
|
|
618
|
-
else:
|
|
619
|
-
selected_col2 = None
|
|
620
|
-
else:
|
|
621
|
-
selected_col1 = None
|
|
622
|
-
selected_col2 = None
|
|
623
|
-
else:
|
|
624
|
-
selected_col1 = None
|
|
625
|
-
selected_col2 = None
|
|
612
|
+
selected = self._get_selected_columns(max_cols=2)
|
|
613
|
+
selected_col1 = selected[0] if len(selected) > 0 else None
|
|
614
|
+
selected_col2 = selected[1] if len(selected) > 1 else None
|
|
626
615
|
|
|
627
616
|
self.addiWidget = OperationOnColsWidget(
|
|
628
617
|
self, column1=selected_col1, column2=selected_col2, operation="add"
|
|
@@ -630,23 +619,9 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
630
619
|
self.addiWidget.show()
|
|
631
620
|
|
|
632
621
|
def subtract_signals(self):
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
637
|
-
cols = np.array(list(self.data.columns))
|
|
638
|
-
if len(col_idx) > 0:
|
|
639
|
-
selected_col1 = str(cols[col_idx[0]])
|
|
640
|
-
if len(col_idx) > 1:
|
|
641
|
-
selected_col2 = str(cols[col_idx[1]])
|
|
642
|
-
else:
|
|
643
|
-
selected_col2 = None
|
|
644
|
-
else:
|
|
645
|
-
selected_col1 = None
|
|
646
|
-
selected_col2 = None
|
|
647
|
-
else:
|
|
648
|
-
selected_col1 = None
|
|
649
|
-
selected_col2 = None
|
|
622
|
+
selected = self._get_selected_columns(max_cols=2)
|
|
623
|
+
selected_col1 = selected[0] if len(selected) > 0 else None
|
|
624
|
+
selected_col2 = selected[1] if len(selected) > 1 else None
|
|
650
625
|
|
|
651
626
|
self.subWidget = OperationOnColsWidget(
|
|
652
627
|
self, column1=selected_col1, column2=selected_col2, operation="subtract"
|
|
@@ -654,56 +629,22 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
654
629
|
self.subWidget.show()
|
|
655
630
|
|
|
656
631
|
def differenciate_selected_feature(self):
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
# create new col
|
|
661
|
-
|
|
662
|
-
x = self.table_view.selectedIndexes()
|
|
663
|
-
col_idx = np.unique(np.array([l.column() for l in x]))
|
|
664
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
665
|
-
cols = np.array(list(self.data.columns))
|
|
666
|
-
if len(col_idx) > 0:
|
|
667
|
-
selected_col = str(cols[col_idx[0]])
|
|
668
|
-
else:
|
|
669
|
-
selected_col = None
|
|
670
|
-
else:
|
|
671
|
-
selected_col = None
|
|
672
|
-
|
|
632
|
+
"""Open widget to differentiate the selected column."""
|
|
633
|
+
selected = self._get_selected_columns(max_cols=1)
|
|
634
|
+
selected_col = selected[0] if selected else None
|
|
673
635
|
self.diffWidget = DifferentiateColWidget(self, selected_col)
|
|
674
636
|
self.diffWidget.show()
|
|
675
637
|
|
|
676
638
|
def take_log_of_selected_feature(self):
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
# create new col
|
|
681
|
-
|
|
682
|
-
x = self.table_view.selectedIndexes()
|
|
683
|
-
col_idx = np.unique(np.array([l.column() for l in x]))
|
|
684
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
685
|
-
cols = np.array(list(self.data.columns))
|
|
686
|
-
if len(col_idx) > 0:
|
|
687
|
-
selected_col = str(cols[col_idx[0]])
|
|
688
|
-
else:
|
|
689
|
-
selected_col = None
|
|
690
|
-
else:
|
|
691
|
-
selected_col = None
|
|
692
|
-
|
|
639
|
+
"""Open widget to take log of the selected column."""
|
|
640
|
+
selected = self._get_selected_columns(max_cols=1)
|
|
641
|
+
selected_col = selected[0] if selected else None
|
|
693
642
|
self.LogWidget = LogColWidget(self, selected_col)
|
|
694
643
|
self.LogWidget.show()
|
|
695
644
|
|
|
696
645
|
def merge_classification_features(self):
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
col_idx = np.unique(np.array([l.column() for l in x]))
|
|
700
|
-
|
|
701
|
-
col_selection = []
|
|
702
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
703
|
-
cols = np.array(list(self.data.columns))
|
|
704
|
-
if len(col_idx) > 0:
|
|
705
|
-
selected_cols = cols[col_idx]
|
|
706
|
-
col_selection.extend(selected_cols)
|
|
646
|
+
"""Open widget to merge selected classification columns."""
|
|
647
|
+
col_selection = self._get_selected_columns()
|
|
707
648
|
|
|
708
649
|
# Lazy load MergeGroupWidget
|
|
709
650
|
from celldetective.gui.table_ops._merge_groups import MergeGroupWidget
|
|
@@ -712,38 +653,16 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
712
653
|
self.merge_classification_widget.show()
|
|
713
654
|
|
|
714
655
|
def calibrate_selected_feature(self):
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
719
|
-
cols = np.array(list(self.data.columns))
|
|
720
|
-
if len(col_idx) > 0:
|
|
721
|
-
selected_col = str(cols[col_idx[0]])
|
|
722
|
-
else:
|
|
723
|
-
selected_col = None
|
|
724
|
-
else:
|
|
725
|
-
selected_col = None
|
|
726
|
-
|
|
656
|
+
"""Open widget to calibrate the selected column."""
|
|
657
|
+
selected = self._get_selected_columns(max_cols=1)
|
|
658
|
+
selected_col = selected[0] if selected else None
|
|
727
659
|
self.calWidget = CalibrateColWidget(self, selected_col)
|
|
728
660
|
self.calWidget.show()
|
|
729
661
|
|
|
730
662
|
def take_abs_of_selected_feature(self):
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
# create new col
|
|
735
|
-
|
|
736
|
-
x = self.table_view.selectedIndexes()
|
|
737
|
-
col_idx = np.unique(np.array([l.column() for l in x]))
|
|
738
|
-
if isinstance(col_idx, (list, np.ndarray)):
|
|
739
|
-
cols = np.array(list(self.data.columns))
|
|
740
|
-
if len(col_idx) > 0:
|
|
741
|
-
selected_col = str(cols[col_idx[0]])
|
|
742
|
-
else:
|
|
743
|
-
selected_col = None
|
|
744
|
-
else:
|
|
745
|
-
selected_col = None
|
|
746
|
-
|
|
663
|
+
"""Open widget to take absolute value of the selected column."""
|
|
664
|
+
selected = self._get_selected_columns(max_cols=1)
|
|
665
|
+
selected_col = selected[0] if selected else None
|
|
747
666
|
self.absWidget = AbsColWidget(self, selected_col)
|
|
748
667
|
self.absWidget.show()
|
|
749
668
|
|
|
@@ -991,7 +910,8 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
991
910
|
y = column_names[unique_cols]
|
|
992
911
|
idx = self.y_cb.findText(y)
|
|
993
912
|
self.y_cb.setCurrentIndex(idx)
|
|
994
|
-
except:
|
|
913
|
+
except (IndexError, KeyError):
|
|
914
|
+
# No column selected or invalid selection
|
|
995
915
|
pass
|
|
996
916
|
|
|
997
917
|
hbox = QHBoxLayout()
|
|
@@ -1018,7 +938,8 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
1018
938
|
if hasattr(matplotlib.cm, str(cm).lower()):
|
|
1019
939
|
try:
|
|
1020
940
|
self.cmap_cb.addColormap(cm.lower())
|
|
1021
|
-
except:
|
|
941
|
+
except Exception:
|
|
942
|
+
# Some colormaps may fail to add
|
|
1022
943
|
pass
|
|
1023
944
|
|
|
1024
945
|
hbox = QHBoxLayout()
|
|
@@ -1057,7 +978,7 @@ class TableUI(CelldetectiveMainWindow):
|
|
|
1057
978
|
cmap(i / len(self.data[self.hue_variable].unique()))
|
|
1058
979
|
for i in range(len(self.data[self.hue_variable].unique()))
|
|
1059
980
|
]
|
|
1060
|
-
except:
|
|
981
|
+
except (KeyError, ZeroDivisionError):
|
|
1061
982
|
colors = None
|
|
1062
983
|
|
|
1063
984
|
if self.hue_cb.currentText() == "--":
|
|
@@ -74,6 +74,7 @@ class ThresholdConfigWizard(CelldetectiveMainWindow):
|
|
|
74
74
|
|
|
75
75
|
super().__init__()
|
|
76
76
|
self.parent_window = parent_window
|
|
77
|
+
# Navigate explicit parent chain: SegModelLoader -> ControlPanel -> ProcessPanel -> MainWindow
|
|
77
78
|
self.screen_height = (
|
|
78
79
|
self.parent_window.parent_window.parent_window.parent_window.screen_height
|
|
79
80
|
)
|
|
@@ -124,6 +125,17 @@ class ThresholdConfigWizard(CelldetectiveMainWindow):
|
|
|
124
125
|
self.bg_loader = BackgroundLoader()
|
|
125
126
|
self.bg_loader.start()
|
|
126
127
|
|
|
128
|
+
def closeEvent(self, event):
|
|
129
|
+
"""Clean up resources on close."""
|
|
130
|
+
if hasattr(self, "bg_loader") and self.bg_loader.isRunning():
|
|
131
|
+
self.bg_loader.quit()
|
|
132
|
+
self.bg_loader.wait()
|
|
133
|
+
# Clear large arrays
|
|
134
|
+
for attr in ["img", "labels", "edt_map", "props", "coords"]:
|
|
135
|
+
if hasattr(self, attr):
|
|
136
|
+
delattr(self, attr)
|
|
137
|
+
super().closeEvent(event)
|
|
138
|
+
|
|
127
139
|
def _create_menu_bar(self):
|
|
128
140
|
menu_bar = self.menuBar()
|
|
129
141
|
# Creating menus using a QMenu object
|
|
@@ -810,7 +822,8 @@ class ThresholdConfigWizard(CelldetectiveMainWindow):
|
|
|
810
822
|
for i in range(2):
|
|
811
823
|
try:
|
|
812
824
|
self.features_cb[i].disconnect()
|
|
813
|
-
except
|
|
825
|
+
except TypeError:
|
|
826
|
+
# No connections to disconnect
|
|
814
827
|
pass
|
|
815
828
|
self.features_cb[i].clear()
|
|
816
829
|
|
|
@@ -879,12 +892,39 @@ class ThresholdConfigWizard(CelldetectiveMainWindow):
|
|
|
879
892
|
self.exp_dir + f"configs/threshold_config_{self.mode}.json",
|
|
880
893
|
"JSON (*.json)",
|
|
881
894
|
)[0]
|
|
882
|
-
|
|
883
|
-
|
|
895
|
+
|
|
896
|
+
if not self.previous_instruction_file:
|
|
897
|
+
return # User cancelled
|
|
898
|
+
|
|
899
|
+
try:
|
|
900
|
+
with open(self.previous_instruction_file, "r") as f:
|
|
901
|
+
threshold_instructions = json.load(f)
|
|
902
|
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
|
903
|
+
generic_message(f"Could not load config: {e}")
|
|
904
|
+
return
|
|
905
|
+
|
|
906
|
+
# Validate required keys
|
|
907
|
+
required_keys = [
|
|
908
|
+
"target_channel",
|
|
909
|
+
"filters",
|
|
910
|
+
"thresholds",
|
|
911
|
+
"marker_footprint_size",
|
|
912
|
+
"marker_min_distance",
|
|
913
|
+
"feature_queries",
|
|
914
|
+
]
|
|
915
|
+
missing_keys = [k for k in required_keys if k not in threshold_instructions]
|
|
916
|
+
if missing_keys:
|
|
917
|
+
generic_message(f"Config file is missing required keys: {missing_keys}")
|
|
918
|
+
return
|
|
884
919
|
|
|
885
920
|
target_channel = threshold_instructions["target_channel"]
|
|
886
|
-
index = self.viewer.
|
|
887
|
-
|
|
921
|
+
index = self.viewer.channel_cb.findText(target_channel)
|
|
922
|
+
if index >= 0:
|
|
923
|
+
self.viewer.channel_cb.setCurrentIndex(index)
|
|
924
|
+
else:
|
|
925
|
+
logger.warning(
|
|
926
|
+
f"Channel '{target_channel}' not found in available channels"
|
|
927
|
+
)
|
|
888
928
|
|
|
889
929
|
filters = threshold_instructions["filters"]
|
|
890
930
|
items_to_add = [f[0] + "_filter" for f in filters]
|
|
@@ -5,18 +5,34 @@ from pathlib import Path
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from PyQt5.QtCore import QSize
|
|
7
7
|
from PyQt5.QtGui import QDoubleValidator
|
|
8
|
-
from PyQt5.QtWidgets import
|
|
8
|
+
from PyQt5.QtWidgets import (
|
|
9
|
+
QMessageBox,
|
|
10
|
+
QHBoxLayout,
|
|
11
|
+
QLabel,
|
|
12
|
+
QComboBox,
|
|
13
|
+
QLineEdit,
|
|
14
|
+
QPushButton,
|
|
15
|
+
QCheckBox,
|
|
16
|
+
QSizePolicy,
|
|
17
|
+
QWidget,
|
|
18
|
+
)
|
|
19
|
+
from PyQt5.QtCore import Qt
|
|
20
|
+
from celldetective.gui.base.utils import center_window
|
|
9
21
|
from fonticon_mdi6 import MDI6
|
|
10
22
|
from natsort import natsorted
|
|
11
23
|
from superqt.fonticon import icon
|
|
12
24
|
|
|
13
|
-
from celldetective.gui.gui_utils import PreprocessingLayout2
|
|
14
25
|
from celldetective.gui.viewers.base_viewer import StackVisualizer
|
|
26
|
+
from celldetective.gui.gui_utils import PreprocessingLayout2
|
|
15
27
|
from celldetective.utils.image_loaders import load_frames
|
|
28
|
+
from celldetective.measure import extract_blobs_in_image
|
|
29
|
+
from celldetective.filters import filter_image
|
|
16
30
|
from celldetective import get_logger
|
|
31
|
+
from tifffile import imread
|
|
17
32
|
|
|
18
33
|
logger = get_logger(__name__)
|
|
19
34
|
|
|
35
|
+
|
|
20
36
|
class SpotDetectionVisualizer(StackVisualizer):
|
|
21
37
|
|
|
22
38
|
def __init__(
|
|
@@ -37,6 +53,7 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
37
53
|
self.labels = labels
|
|
38
54
|
self.detection_channel = self.target_channel
|
|
39
55
|
self.switch_from_channel = False
|
|
56
|
+
self.preview_preprocessing = False
|
|
40
57
|
|
|
41
58
|
self.parent_channel_cb = parent_channel_cb
|
|
42
59
|
self.parent_diameter_le = parent_diameter_le
|
|
@@ -47,6 +64,35 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
47
64
|
self.floatValidator = QDoubleValidator()
|
|
48
65
|
self.init_scatter()
|
|
49
66
|
|
|
67
|
+
self.setWindowTitle(self.window_title)
|
|
68
|
+
self.resize(1200, 800)
|
|
69
|
+
|
|
70
|
+
# Main Layout (Horizontal split)
|
|
71
|
+
self.main_layout = QHBoxLayout(self)
|
|
72
|
+
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
|
73
|
+
|
|
74
|
+
# Left Panel (Settings) - Scrollable
|
|
75
|
+
from PyQt5.QtWidgets import QScrollArea, QWidget, QVBoxLayout
|
|
76
|
+
|
|
77
|
+
self.scroll_area = QScrollArea()
|
|
78
|
+
self.scroll_area.setWidgetResizable(True)
|
|
79
|
+
self.settings_widget = QWidget()
|
|
80
|
+
self.settings_layout = QVBoxLayout(self.settings_widget)
|
|
81
|
+
self.settings_layout.setContentsMargins(10, 10, 10, 10)
|
|
82
|
+
self.settings_layout.setSpacing(15)
|
|
83
|
+
self.settings_layout.setAlignment(Qt.AlignTop)
|
|
84
|
+
self.scroll_area.setWidget(self.settings_widget)
|
|
85
|
+
self.scroll_area.setFixedWidth(350) # Set a reasonable width for settings
|
|
86
|
+
|
|
87
|
+
# Add Left Panel
|
|
88
|
+
self.main_layout.addWidget(self.scroll_area)
|
|
89
|
+
|
|
90
|
+
# Right Panel (Image Canvas)
|
|
91
|
+
# self.canvas is created by super().__init__
|
|
92
|
+
# We allow it to expand
|
|
93
|
+
self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
94
|
+
self.main_layout.addWidget(self.canvas)
|
|
95
|
+
|
|
50
96
|
self.generate_detection_channel()
|
|
51
97
|
self.detection_channel = self.detection_channel_cb.currentIndex()
|
|
52
98
|
|
|
@@ -57,12 +103,29 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
57
103
|
|
|
58
104
|
self.ax.callbacks.connect("xlim_changed", self.update_marker_sizes)
|
|
59
105
|
self.ax.callbacks.connect("ylim_changed", self.update_marker_sizes)
|
|
106
|
+
self._axis_callbacks_connected = True
|
|
60
107
|
|
|
61
108
|
self.apply_diam_btn.clicked.connect(self.detect_and_display_spots)
|
|
62
109
|
self.apply_thresh_btn.clicked.connect(self.detect_and_display_spots)
|
|
63
110
|
|
|
64
|
-
self.channel_cb.setCurrentIndex(self.target_channel)
|
|
65
|
-
self.detection_channel_cb.setCurrentIndex(
|
|
111
|
+
self.channel_cb.setCurrentIndex(min(self.target_channel, self.n_channels - 1))
|
|
112
|
+
self.detection_channel_cb.setCurrentIndex(
|
|
113
|
+
min(self.target_channel, self.n_channels - 1)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def closeEvent(self, event):
|
|
117
|
+
"""Clean up resources on close."""
|
|
118
|
+
# Clear large arrays
|
|
119
|
+
self.target_img = None
|
|
120
|
+
self.init_label = None
|
|
121
|
+
self.spot_sizes = []
|
|
122
|
+
|
|
123
|
+
# Remove scatter
|
|
124
|
+
if hasattr(self, "spot_scat") and self.spot_scat is not None:
|
|
125
|
+
self.spot_scat.remove()
|
|
126
|
+
self.spot_scat = None
|
|
127
|
+
|
|
128
|
+
super().closeEvent(event)
|
|
66
129
|
|
|
67
130
|
def update_marker_sizes(self, event=None):
|
|
68
131
|
|
|
@@ -105,9 +168,7 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
105
168
|
if not self.switch_from_channel:
|
|
106
169
|
self.reset_detection()
|
|
107
170
|
|
|
108
|
-
if self.mode == "virtual":
|
|
109
|
-
from tifffile import imread
|
|
110
|
-
|
|
171
|
+
if self.mode == "virtual" and hasattr(self, "mask_paths"):
|
|
111
172
|
self.init_label = imread(self.mask_paths[value])
|
|
112
173
|
self.target_img = load_frames(
|
|
113
174
|
self.img_num_per_channel[self.detection_channel, value],
|
|
@@ -122,15 +183,11 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
122
183
|
|
|
123
184
|
self.reset_detection()
|
|
124
185
|
self.control_valid_parameters() # set current diam and threshold
|
|
125
|
-
# self.change_frame(self.frame_slider.value())
|
|
126
|
-
# self.set_detection_channel_index(self.detection_channel_cb.currentIndex())
|
|
127
186
|
|
|
128
187
|
image_preprocessing = self.preprocessing.list.items
|
|
129
188
|
if image_preprocessing == []:
|
|
130
189
|
image_preprocessing = None
|
|
131
190
|
|
|
132
|
-
from celldetective.measure import extract_blobs_in_image
|
|
133
|
-
|
|
134
191
|
blobs_filtered = extract_blobs_in_image(
|
|
135
192
|
self.target_img,
|
|
136
193
|
self.init_label,
|
|
@@ -157,8 +214,7 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
157
214
|
self.canvas.canvas.draw()
|
|
158
215
|
|
|
159
216
|
def reset_detection(self):
|
|
160
|
-
|
|
161
|
-
self.ax.scatter([], []).get_offsets()
|
|
217
|
+
"""Clear spot detection display."""
|
|
162
218
|
empty_offset = np.ma.masked_array([0, 0], mask=True)
|
|
163
219
|
self.spot_scat.set_offsets(empty_offset)
|
|
164
220
|
self.canvas.canvas.draw()
|
|
@@ -205,8 +261,6 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
205
261
|
returnValue = msgBox.exec()
|
|
206
262
|
self.close()
|
|
207
263
|
|
|
208
|
-
from tifffile import imread
|
|
209
|
-
|
|
210
264
|
self.init_label = imread(self.mask_paths[self.frame_slider.value()])
|
|
211
265
|
|
|
212
266
|
def generate_detection_channel(self):
|
|
@@ -215,7 +269,7 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
215
269
|
assert len(self.channel_names) == self.n_channels
|
|
216
270
|
|
|
217
271
|
channel_layout = QHBoxLayout()
|
|
218
|
-
channel_layout.setContentsMargins(
|
|
272
|
+
channel_layout.setContentsMargins(0, 0, 0, 0)
|
|
219
273
|
channel_layout.addWidget(QLabel("Detection\nchannel: "), 25)
|
|
220
274
|
|
|
221
275
|
self.detection_channel_cb = QComboBox()
|
|
@@ -231,11 +285,22 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
231
285
|
# self.invert_check.toggled.connect(self.set_invert)
|
|
232
286
|
# channel_layout.addWidget(self.invert_check, 10)
|
|
233
287
|
|
|
234
|
-
self.
|
|
288
|
+
self.settings_layout.addLayout(channel_layout)
|
|
235
289
|
|
|
236
|
-
self.
|
|
237
|
-
self.
|
|
238
|
-
|
|
290
|
+
self.preview_cb = QCheckBox("Preview")
|
|
291
|
+
self.preview_cb.toggled.connect(self.toggle_preprocessing_preview)
|
|
292
|
+
|
|
293
|
+
self.preprocessing = PreprocessingLayout2(
|
|
294
|
+
fraction=25, parent_window=self, extra_widget=self.preview_cb
|
|
295
|
+
)
|
|
296
|
+
self.preprocessing.setContentsMargins(0, 10, 0, 10)
|
|
297
|
+
self.preprocessing.list.list_widget.model().rowsInserted.connect(
|
|
298
|
+
self.update_preview_if_active
|
|
299
|
+
)
|
|
300
|
+
self.preprocessing.list.list_widget.model().rowsRemoved.connect(
|
|
301
|
+
self.update_preview_if_active
|
|
302
|
+
)
|
|
303
|
+
self.settings_layout.addLayout(self.preprocessing)
|
|
239
304
|
|
|
240
305
|
# def set_invert(self):
|
|
241
306
|
# if self.invert_check.isChecked():
|
|
@@ -272,19 +337,22 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
272
337
|
self.spot_diam_le.textChanged.connect(self.control_valid_parameters)
|
|
273
338
|
self.spot_thresh_le.textChanged.connect(self.control_valid_parameters)
|
|
274
339
|
|
|
340
|
+
self.apply_diam_btn.clicked.connect(self.detect_and_display_spots)
|
|
341
|
+
self.apply_thresh_btn.clicked.connect(self.detect_and_display_spots)
|
|
342
|
+
|
|
275
343
|
spot_diam_layout = QHBoxLayout()
|
|
276
|
-
spot_diam_layout.setContentsMargins(
|
|
344
|
+
spot_diam_layout.setContentsMargins(0, 0, 0, 0)
|
|
277
345
|
spot_diam_layout.addWidget(QLabel("Spot diameter: "), 25)
|
|
278
346
|
spot_diam_layout.addWidget(self.spot_diam_le, 65)
|
|
279
347
|
spot_diam_layout.addWidget(self.apply_diam_btn, 10)
|
|
280
|
-
self.
|
|
348
|
+
self.settings_layout.addLayout(spot_diam_layout)
|
|
281
349
|
|
|
282
350
|
spot_thresh_layout = QHBoxLayout()
|
|
283
|
-
spot_thresh_layout.setContentsMargins(
|
|
351
|
+
spot_thresh_layout.setContentsMargins(0, 0, 0, 0)
|
|
284
352
|
spot_thresh_layout.addWidget(QLabel("Detection\nthreshold: "), 25)
|
|
285
353
|
spot_thresh_layout.addWidget(self.spot_thresh_le, 65)
|
|
286
354
|
spot_thresh_layout.addWidget(self.apply_thresh_btn, 10)
|
|
287
|
-
self.
|
|
355
|
+
self.settings_layout.addLayout(spot_thresh_layout)
|
|
288
356
|
|
|
289
357
|
def generate_add_measurement_btn(self):
|
|
290
358
|
|
|
@@ -297,7 +365,48 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
297
365
|
add_hbox.addWidget(QLabel(""), 33)
|
|
298
366
|
add_hbox.addWidget(self.add_measurement_btn, 33)
|
|
299
367
|
add_hbox.addWidget(QLabel(""), 33)
|
|
300
|
-
self.
|
|
368
|
+
self.settings_layout.addLayout(add_hbox)
|
|
369
|
+
|
|
370
|
+
def show(self):
|
|
371
|
+
QWidget.show(self)
|
|
372
|
+
center_window(self)
|
|
373
|
+
|
|
374
|
+
def update_preview_if_active(self):
|
|
375
|
+
if self.preview_cb.isChecked():
|
|
376
|
+
self.toggle_preprocessing_preview()
|
|
377
|
+
|
|
378
|
+
def toggle_preprocessing_preview(self):
|
|
379
|
+
|
|
380
|
+
image_preprocessing = self.preprocessing.list.items
|
|
381
|
+
if image_preprocessing == []:
|
|
382
|
+
image_preprocessing = None
|
|
383
|
+
|
|
384
|
+
if self.preview_cb.isChecked() and image_preprocessing is not None:
|
|
385
|
+
# Apply preprocessing
|
|
386
|
+
try:
|
|
387
|
+
preprocessed_img = filter_image(
|
|
388
|
+
self.target_img.copy(), filters=image_preprocessing
|
|
389
|
+
)
|
|
390
|
+
self.im.set_data(preprocessed_img)
|
|
391
|
+
|
|
392
|
+
# Update contrast to match new range
|
|
393
|
+
p01 = np.nanpercentile(preprocessed_img, 0.1)
|
|
394
|
+
p99 = np.nanpercentile(preprocessed_img, 99.9)
|
|
395
|
+
self.im.set_clim(vmin=p01, vmax=p99)
|
|
396
|
+
if hasattr(self, "contrast_slider"):
|
|
397
|
+
self.contrast_slider.setValue((p01, p99))
|
|
398
|
+
self.canvas.draw()
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error(f"Preprocessing preview failed: {e}")
|
|
401
|
+
else:
|
|
402
|
+
# Restore original
|
|
403
|
+
self.im.set_data(self.target_img)
|
|
404
|
+
p01 = np.nanpercentile(self.target_img, 0.1)
|
|
405
|
+
p99 = np.nanpercentile(self.target_img, 99.9)
|
|
406
|
+
self.im.set_clim(vmin=p01, vmax=p99)
|
|
407
|
+
if hasattr(self, "contrast_slider"):
|
|
408
|
+
self.contrast_slider.setValue((p01, p99))
|
|
409
|
+
self.canvas.draw()
|
|
301
410
|
|
|
302
411
|
def control_valid_parameters(self):
|
|
303
412
|
|
|
@@ -305,14 +414,14 @@ class SpotDetectionVisualizer(StackVisualizer):
|
|
|
305
414
|
try:
|
|
306
415
|
self.diameter = float(self.spot_diam_le.text().replace(",", "."))
|
|
307
416
|
valid_diam = True
|
|
308
|
-
except:
|
|
417
|
+
except ValueError:
|
|
309
418
|
valid_diam = False
|
|
310
419
|
|
|
311
420
|
valid_thresh = False
|
|
312
421
|
try:
|
|
313
422
|
self.thresh = float(self.spot_thresh_le.text().replace(",", "."))
|
|
314
423
|
valid_thresh = True
|
|
315
|
-
except:
|
|
424
|
+
except ValueError:
|
|
316
425
|
valid_thresh = False
|
|
317
426
|
|
|
318
427
|
if valid_diam and valid_thresh:
|