celldetective 1.5.0b8__py3-none-any.whl → 1.5.0b10__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]