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.
@@ -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 DifferentiateColWidget, OperationOnColsWidget, CalibrateColWidget, \
25
- AbsColWidget, LogColWidget
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
- print("tab to show: ", self.data)
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
- print("Saving each table in its respective position folder...")
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
- print("Done...")
589
+ logger.info("Done saving tables.")
559
590
 
560
591
  def divide_signals(self):
561
-
562
- x = self.table_view.selectedIndexes()
563
- col_idx = np.unique(np.array([l.column() for l in x]))
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
- x = self.table_view.selectedIndexes()
587
- col_idx = np.unique(np.array([l.column() for l in x]))
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
- x = self.table_view.selectedIndexes()
611
- col_idx = np.unique(np.array([l.column() for l in x]))
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
- x = self.table_view.selectedIndexes()
635
- col_idx = np.unique(np.array([l.column() for l in x]))
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
- # check only one col selected and assert is numerical
659
- # open widget to select window parameters, directionality
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
- # check only one col selected and assert is numerical
679
- # open widget to select window parameters, directionality
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
- x = self.table_view.selectedIndexes()
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
- x = self.table_view.selectedIndexes()
717
- col_idx = np.unique(np.array([l.column() for l in x]))
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
- # check only one col selected and assert is numerical
733
- # open widget to select window parameters, directionality
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 Exception as _:
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
- with open(self.previous_instruction_file, "r") as f:
883
- threshold_instructions = json.load(f)
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.channels_cb.findText(target_channel)
887
- self.viewer.channels_cb.setCurrentIndex(index)
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 QMessageBox, QHBoxLayout, QLabel, QComboBox, QLineEdit, QPushButton
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(self.target_channel)
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(15, 0, 15, 0)
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.canvas.layout.addLayout(channel_layout)
288
+ self.settings_layout.addLayout(channel_layout)
235
289
 
236
- self.preprocessing = PreprocessingLayout2(fraction=25, parent_window=self)
237
- self.preprocessing.setContentsMargins(15, 0, 15, 0)
238
- self.canvas.layout.addLayout(self.preprocessing)
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(15, 0, 15, 0)
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.canvas.layout.addLayout(spot_diam_layout)
348
+ self.settings_layout.addLayout(spot_diam_layout)
281
349
 
282
350
  spot_thresh_layout = QHBoxLayout()
283
- spot_thresh_layout.setContentsMargins(15, 0, 15, 0)
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.canvas.layout.addLayout(spot_thresh_layout)
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.canvas.layout.addLayout(add_hbox)
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: