celldetective 1.3.0.post1__tar.gz → 1.3.1__tar.gz

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.
Files changed (109) hide show
  1. {celldetective-1.3.0.post1 → celldetective-1.3.1}/PKG-INFO +3 -3
  2. {celldetective-1.3.0.post1 → celldetective-1.3.1}/README.md +2 -2
  3. celldetective-1.3.1/celldetective/_version.py +1 -0
  4. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/events.py +86 -11
  5. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/extra_properties.py +5 -1
  6. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/InitWindow.py +35 -9
  7. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/classifier_widget.py +50 -22
  8. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/layouts.py +128 -7
  9. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/measurement_options.py +3 -3
  10. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/process_block.py +46 -12
  11. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/retrain_segmentation_model_options.py +24 -10
  12. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/survival_ui.py +19 -2
  13. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/viewers.py +263 -3
  14. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/links/zenodo.json +136 -123
  15. celldetective-1.3.1/celldetective/models/tracking_configs/biased_motion.json +68 -0
  16. celldetective-1.3.1/celldetective/models/tracking_configs/no_z_motion.json +202 -0
  17. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/preprocessing.py +172 -3
  18. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/signals.py +5 -2
  19. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/tracking.py +7 -3
  20. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/utils.py +6 -6
  21. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective.egg-info/PKG-INFO +3 -3
  22. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective.egg-info/SOURCES.txt +2 -0
  23. celldetective-1.3.0.post1/celldetective/_version.py +0 -1
  24. {celldetective-1.3.0.post1 → celldetective-1.3.1}/LICENSE +0 -0
  25. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/__init__.py +0 -0
  26. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/__main__.py +0 -0
  27. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/datasets/segmentation_annotations/blank +0 -0
  28. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/datasets/signal_annotations/blank +0 -0
  29. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/filters.py +0 -0
  30. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/__init__.py +0 -0
  31. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/about.py +0 -0
  32. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/analyze_block.py +0 -0
  33. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/btrack_options.py +0 -0
  34. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/configure_new_exp.py +0 -0
  35. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/control_panel.py +0 -0
  36. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/generic_signal_plot.py +0 -0
  37. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/gui_utils.py +0 -0
  38. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/DL-segmentation-strategy.json +0 -0
  39. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/Threshold-vs-DL.json +0 -0
  40. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/cell-populations.json +0 -0
  41. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/exp-structure.json +0 -0
  42. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/feature-btrack.json +0 -0
  43. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/neighborhood.json +0 -0
  44. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/prefilter-for-segmentation.json +0 -0
  45. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/preprocessing.json +0 -0
  46. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/propagate-classification.json +0 -0
  47. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/track-postprocessing.json +0 -0
  48. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/help/tracking.json +0 -0
  49. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/json_readers.py +0 -0
  50. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/neighborhood_options.py +0 -0
  51. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/plot_measurements.py +0 -0
  52. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/plot_signals_ui.py +0 -0
  53. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/retrain_signal_model_options.py +0 -0
  54. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/seg_model_loader.py +0 -0
  55. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/signal_annotator.py +0 -0
  56. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/signal_annotator2.py +0 -0
  57. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/signal_annotator_options.py +0 -0
  58. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/styles.py +0 -0
  59. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/tableUI.py +0 -0
  60. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/gui/thresholds_gui.py +0 -0
  61. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/logo-large.png +0 -0
  62. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/logo.png +0 -0
  63. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/signals_icon.png +0 -0
  64. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/splash-test.png +0 -0
  65. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/splash.png +0 -0
  66. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/splash0.png +0 -0
  67. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/survival2.png +0 -0
  68. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/vignette_signals2.png +0 -0
  69. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/icons/vignette_signals2.svg +0 -0
  70. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/io.py +0 -0
  71. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/measure.py +0 -0
  72. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/models/pair_signal_detection/blank +0 -0
  73. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/models/segmentation_effectors/blank +0 -0
  74. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/models/segmentation_generic/blank +0 -0
  75. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/models/segmentation_targets/blank +0 -0
  76. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/models/signal_detection/blank +0 -0
  77. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/models/tracking_configs/mcf7.json +0 -0
  78. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/models/tracking_configs/ricm.json +0 -0
  79. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/models/tracking_configs/ricm2.json +0 -0
  80. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/neighborhood.py +0 -0
  81. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/relative_measurements.py +0 -0
  82. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/scripts/analyze_signals.py +0 -0
  83. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/scripts/measure_cells.py +0 -0
  84. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/scripts/measure_relative.py +0 -0
  85. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/scripts/segment_cells.py +0 -0
  86. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/scripts/segment_cells_thresholds.py +0 -0
  87. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/scripts/track_cells.py +0 -0
  88. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/scripts/train_segmentation_model.py +0 -0
  89. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/scripts/train_signal_model.py +0 -0
  90. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective/segmentation.py +0 -0
  91. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective.egg-info/dependency_links.txt +0 -0
  92. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective.egg-info/entry_points.txt +0 -0
  93. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective.egg-info/not-zip-safe +0 -0
  94. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective.egg-info/requires.txt +0 -0
  95. {celldetective-1.3.0.post1 → celldetective-1.3.1}/celldetective.egg-info/top_level.txt +0 -0
  96. {celldetective-1.3.0.post1 → celldetective-1.3.1}/setup.cfg +0 -0
  97. {celldetective-1.3.0.post1 → celldetective-1.3.1}/setup.py +0 -0
  98. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/__init__.py +0 -0
  99. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_events.py +0 -0
  100. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_filters.py +0 -0
  101. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_io.py +0 -0
  102. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_measure.py +0 -0
  103. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_neighborhood.py +0 -0
  104. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_preprocessing.py +0 -0
  105. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_qt.py +0 -0
  106. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_segmentation.py +0 -0
  107. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_signals.py +0 -0
  108. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_tracking.py +0 -0
  109. {celldetective-1.3.0.post1 → celldetective-1.3.1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: celldetective
3
- Version: 1.3.0.post1
3
+ Version: 1.3.1
4
4
  Summary: description
5
5
  Home-page: http://github.com/remyeltorro/celldetective
6
6
  Author: Rémy Torro
@@ -69,7 +69,7 @@ analysis on multimodal time lapse microscopy images.
69
69
 
70
70
  ## Overview
71
71
 
72
- ![Pipeline](https://github.com/remyeltorro/celldetective/raw/main/docs/source/_static/celldetective-blocks.png)
72
+ ![Pipeline](https://github.com/celldetective/celldetective/raw/main/docs/source/_static/celldetective-blocks.png)
73
73
 
74
74
 
75
75
  Celldetective was designed to analyze time-lapse microscopy images in difficult situations: mixed cell populations that are only separable through multimodal information. This software provides a toolkit for the analysis of cell population interactions.
@@ -88,7 +88,7 @@ Instead of reinventing the wheel and out of respect for the amazing work done by
88
88
 
89
89
  **Target Audience**: The software is targeted to scientists who are interested in quantifying dynamically (or not) cell populations from microscopy images. Experimental scientists who produce such images can also analyze their data, thanks to the graphical interface, that completely removes the need for coding, and the many helper functions that guide the user in the analysis steps. Finally, the modular structure of Celldetective welcomes users with a partial need.
90
90
 
91
- ![Signal analysis](https://github.com/remyeltorro/celldetective/raw/main/docs/source/_static/signal-annotator.gif)
91
+ ![Signal analysis](https://github.com/celldetective/celldetective/raw/main/docs/source/_static/signal-annotator.gif)
92
92
 
93
93
 
94
94
  # System requirements
@@ -25,7 +25,7 @@ analysis on multimodal time lapse microscopy images.
25
25
 
26
26
  ## Overview
27
27
 
28
- ![Pipeline](https://github.com/remyeltorro/celldetective/raw/main/docs/source/_static/celldetective-blocks.png)
28
+ ![Pipeline](https://github.com/celldetective/celldetective/raw/main/docs/source/_static/celldetective-blocks.png)
29
29
 
30
30
 
31
31
  Celldetective was designed to analyze time-lapse microscopy images in difficult situations: mixed cell populations that are only separable through multimodal information. This software provides a toolkit for the analysis of cell population interactions.
@@ -44,7 +44,7 @@ Instead of reinventing the wheel and out of respect for the amazing work done by
44
44
 
45
45
  **Target Audience**: The software is targeted to scientists who are interested in quantifying dynamically (or not) cell populations from microscopy images. Experimental scientists who produce such images can also analyze their data, thanks to the graphical interface, that completely removes the need for coding, and the many helper functions that guide the user in the analysis steps. Finally, the modular structure of Celldetective welcomes users with a partial need.
46
46
 
47
- ![Signal analysis](https://github.com/remyeltorro/celldetective/raw/main/docs/source/_static/signal-annotator.gif)
47
+ ![Signal analysis](https://github.com/celldetective/celldetective/raw/main/docs/source/_static/signal-annotator.gif)
48
48
 
49
49
 
50
50
  # System requirements
@@ -0,0 +1 @@
1
+ __version__ = "1.3.1"
@@ -1,7 +1,7 @@
1
1
  import numpy as np
2
2
  from lifelines import KaplanMeierFitter
3
3
 
4
- def switch_to_events(classes, event_times, max_times, origin_times=None, left_censored=True, FrameToMin=None):
4
+ def switch_to_events(classes, event_times, max_times, origin_times=None, left_censored=True, FrameToMin=None, cut_observation_time=None):
5
5
 
6
6
 
7
7
  """
@@ -29,6 +29,8 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
29
29
  FrameToMin : float, optional
30
30
  A conversion factor to transform survival times from frames (or any other unit) to minutes. If None, no conversion
31
31
  is applied (default is None).
32
+ cut_observation_time : float or None, optional
33
+ A cutoff time to artificially reduce the observation window and exclude late events. If None, uses all available data (default is None).
32
34
 
33
35
  Returns
34
36
  -------
@@ -70,13 +72,36 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
70
72
  if ot>=0. and ot==ot:
71
73
  # origin time is larger than zero, no censorship
72
74
  if c==0 and t>0:
75
+
73
76
  delta_t = t - ot
74
- if delta_t>0:
75
- events.append(1)
76
- survival_times.append(delta_t)
77
- else:
78
- # negative delta t, invalid cell
79
- pass
77
+
78
+ # Special case: observation cut at arbitrary time
79
+ if cut_observation_time is not None:
80
+ if t>=cut_observation_time:
81
+ # event time larger than cut, becomes no event
82
+ delta_t = cut_observation_time - ot # new time
83
+ if delta_t > 0:
84
+ events.append(0)
85
+ survival_times.append(delta_t)
86
+ else:
87
+ # negative delta t, invalid cell
88
+ pass
89
+ else:
90
+ # still event
91
+ if delta_t > 0:
92
+ events.append(1)
93
+ survival_times.append(delta_t)
94
+ else:
95
+ # negative delta t, invalid cell
96
+ pass
97
+ else:
98
+ # standard mode
99
+ if delta_t>0:
100
+ events.append(1)
101
+ survival_times.append(delta_t)
102
+ else:
103
+ # negative delta t, invalid cell
104
+ pass
80
105
  elif c==1:
81
106
  delta_t = mt - ot
82
107
  if delta_t>0:
@@ -93,10 +118,20 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
93
118
 
94
119
  else:
95
120
  if c==0 and t>0:
96
- events.append(1)
97
- survival_times.append(t - ot)
121
+ if cut_observation_time is not None:
122
+ if t>cut_observation_time:
123
+ events.append(0)
124
+ survival_times.append(cut_observation_time - ot)
125
+ else:
126
+ events.append(1)
127
+ survival_times.append(t - ot)
128
+ else:
129
+ events.append(1)
130
+ survival_times.append(t - ot)
98
131
  elif c==1:
99
132
  events.append(0)
133
+ if cut_observation_time is not None:
134
+ mt = cut_observation_time
100
135
  survival_times.append(mt - ot)
101
136
  else:
102
137
  pass
@@ -106,7 +141,47 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
106
141
  survival_times = [s*FrameToMin for s in survival_times]
107
142
  return events, survival_times
108
143
 
109
- def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMin=1):
144
+ def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMin=1, cut_observation_time=None):
145
+
146
+ """
147
+ Computes survival analysis for a specific class of interest within a dataset, returning a fitted Kaplan-Meier
148
+ survival curve based on event and reference times.
149
+
150
+ Parameters
151
+ ----------
152
+ df : pandas.DataFrame
153
+ The dataset containing tracking data, event times, and other relevant columns for survival analysis.
154
+ class_of_interest : str
155
+ The name of the column that specifies the class for which survival analysis is to be computed.
156
+ t_event : str
157
+ The column indicating the time of the event of interest (e.g., cell death or migration stop).
158
+ t_reference : str or None, optional
159
+ The reference column indicating the start or origin time for each track (e.g., detection time). If None,
160
+ events are not left-censored (default is None).
161
+ FrameToMin : float, optional
162
+ Conversion factor to scale the frame time to minutes (default is 1, assuming no scaling).
163
+ cut_observation_time : float or None, optional
164
+ A cutoff time to artificially reduce the observation window and exclude late events. If None, uses all available data (default is None).
165
+ Returns
166
+ -------
167
+ ks : lifelines.KaplanMeierFitter or None
168
+ A fitted Kaplan-Meier estimator object. If there are no events, returns None.
169
+
170
+ Notes
171
+ -----
172
+ - The function groups the data by 'position' and 'TRACK_ID', extracting the minimum `class_of_interest` and `t_event`
173
+ values for each track.
174
+ - If `t_reference` is provided, the analysis assumes left-censoring and will use `t_reference` as the origin time for
175
+ each track.
176
+ - The function calls `switch_to_events` to determine the event occurrences and their associated survival times.
177
+ - A Kaplan-Meier estimator (`KaplanMeierFitter`) is fitted to the data to compute the survival curve.
178
+
179
+ Example
180
+ -------
181
+ >>> ks = compute_survival(df, class_of_interest="class_custom", t_event="time_custom", t_reference="t_firstdetection")
182
+ >>> ks.plot_survival_function()
183
+
184
+ """
110
185
 
111
186
  cols = list(df.columns)
112
187
  assert class_of_interest in cols,"The requested class cannot be found in the dataframe..."
@@ -127,7 +202,7 @@ def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMi
127
202
  assert t_reference in cols,"The reference time cannot be found in the dataframe..."
128
203
  first_detections = df.groupby(['position','TRACK_ID'])[t_reference].max().values
129
204
 
130
- events, survival_times = switch_to_events(classes, event_times, max_times, origin_times=first_detections, left_censored=left_censored, FrameToMin=FrameToMin)
205
+ events, survival_times = switch_to_events(classes, event_times, max_times, origin_times=first_detections, left_censored=left_censored, FrameToMin=FrameToMin, cut_observation_time=cut_observation_time)
131
206
  ks = KaplanMeierFitter()
132
207
  if len(events)>0:
133
208
  ks.fit(survival_times, event_observed=events)
@@ -57,7 +57,11 @@ def intensity_median(regionmask, intensity_image):
57
57
  return np.nanmedian(intensity_image[regionmask])
58
58
 
59
59
  def intensity_nanmean(regionmask, intensity_image):
60
- return np.nanmean(intensity_image[regionmask])
60
+
61
+ if np.all(intensity_image==0):
62
+ return np.nan
63
+ else:
64
+ return np.nanmean(intensity_image[regionmask])
61
65
 
62
66
  def intensity_centre_of_mass_displacement(regionmask, intensity_image):
63
67
 
@@ -1,5 +1,5 @@
1
1
  from PyQt5.QtWidgets import QApplication, QMainWindow
2
- from celldetective.utils import get_software_location
2
+ from celldetective.utils import get_software_location, download_zenodo_file
3
3
  import os
4
4
  from PyQt5.QtWidgets import QFileDialog, QWidget, QVBoxLayout, QCheckBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QMenu, QAction
5
5
  from PyQt5.QtCore import Qt, QUrl
@@ -108,6 +108,10 @@ class AppInitWindow(QMainWindow):
108
108
  for i in range(len(self.recentFileActs)):
109
109
  self.OpenRecentAction.addAction(self.recentFileActs[i])
110
110
 
111
+ fileMenu.addMenu(self.openDemo)
112
+ self.openDemo.addAction(self.openSpreadingAssayDemo)
113
+ self.openDemo.addAction(self.openCytotoxicityAssayDemo)
114
+
111
115
  fileMenu.addAction(self.openModels)
112
116
  fileMenu.addSeparator()
113
117
  fileMenu.addAction(self.exitAction)
@@ -124,7 +128,7 @@ class AppInitWindow(QMainWindow):
124
128
  helpMenu = QMenu("Help", self)
125
129
  helpMenu.clear()
126
130
  helpMenu.addAction(self.DocumentationAction)
127
- helpMenu.addAction(self.SoftwareAction)
131
+ #helpMenu.addAction(self.SoftwareAction)
128
132
  helpMenu.addSeparator()
129
133
  helpMenu.addAction(self.AboutAction)
130
134
  menuBar.addMenu(helpMenu)
@@ -137,13 +141,17 @@ class AppInitWindow(QMainWindow):
137
141
  #self.newAction = QAction(self)
138
142
  #self.newAction.setText("&New")
139
143
  # Creating actions using the second constructor
140
- self.openAction = QAction('Open...', self)
144
+ self.openAction = QAction('Open Project', self)
141
145
  self.openAction.setShortcut("Ctrl+O")
142
146
  self.openAction.setShortcutVisibleInContextMenu(True)
143
147
 
144
- self.MemoryAndThreadsAction = QAction('Memory & Threads...')
148
+ self.openDemo = QMenu('Open Demo')
149
+ self.openSpreadingAssayDemo = QAction('Spreading Assay Demo', self)
150
+ self.openCytotoxicityAssayDemo = QAction('Cytotoxicity Assay Demo', self)
151
+
152
+ self.MemoryAndThreadsAction = QAction('Threads')
145
153
 
146
- self.CorrectAnnotationAction = QAction('Correct a segmentation annotation...')
154
+ self.CorrectAnnotationAction = QAction('Correct a segmentation annotation')
147
155
 
148
156
  self.newExpAction = QAction('New', self)
149
157
  self.newExpAction.setShortcut("Ctrl+N")
@@ -154,14 +162,14 @@ class AppInitWindow(QMainWindow):
154
162
  self.openModels.setShortcut("Ctrl+L")
155
163
  self.openModels.setShortcutVisibleInContextMenu(True)
156
164
 
157
- self.OpenRecentAction = QMenu('Open Recent')
165
+ self.OpenRecentAction = QMenu('Open Recent Project')
158
166
  self.reload_previous_experiments()
159
167
 
160
168
  self.DocumentationAction = QAction("Documentation", self)
161
169
  self.DocumentationAction.setShortcut("Ctrl+D")
162
170
  self.DocumentationAction.setShortcutVisibleInContextMenu(True)
163
171
 
164
- self.SoftwareAction = QAction("Software", self) #1st arg icon(MDI6.information)
172
+ #self.SoftwareAction = QAction("Software", self) #1st arg icon(MDI6.information)
165
173
  self.AboutAction = QAction("About celldetective", self)
166
174
 
167
175
  #self.DocumentationAction.triggered.connect(self.load_previous_config)
@@ -172,9 +180,27 @@ class AppInitWindow(QMainWindow):
172
180
  self.AboutAction.triggered.connect(self.open_about_window)
173
181
  self.MemoryAndThreadsAction.triggered.connect(self.set_memory_and_threads)
174
182
  self.CorrectAnnotationAction.triggered.connect(self.correct_seg_annotation)
175
-
176
183
  self.DocumentationAction.triggered.connect(self.open_documentation)
177
184
 
185
+ self.openSpreadingAssayDemo.triggered.connect(self.download_spreading_assay_demo)
186
+ self.openCytotoxicityAssayDemo.triggered.connect(self.download_cytotoxicity_assay_demo)
187
+
188
+ def download_spreading_assay_demo(self):
189
+
190
+ self.target_dir = str(QFileDialog.getExistingDirectory(self, 'Select Folder for Download'))
191
+ if not os.path.exists(os.sep.join([self.target_dir,'demo_ricm'])):
192
+ download_zenodo_file('demo_ricm', self.target_dir)
193
+ self.experiment_path_selection.setText(os.sep.join([self.target_dir, 'demo_ricm']))
194
+ self.validate_button.click()
195
+
196
+ def download_cytotoxicity_assay_demo(self):
197
+
198
+ self.target_dir = str(QFileDialog.getExistingDirectory(self, 'Select Folder for Download'))
199
+ if not os.path.exists(os.sep.join([self.target_dir,'demo_adcc'])):
200
+ download_zenodo_file('demo_adcc', self.target_dir)
201
+ self.experiment_path_selection.setText(os.sep.join([self.target_dir, 'demo_adcc']))
202
+ self.validate_button.click()
203
+
178
204
  def reload_previous_gpu_threads(self):
179
205
 
180
206
  self.recentFileActs = []
@@ -256,7 +282,7 @@ class AppInitWindow(QMainWindow):
256
282
 
257
283
 
258
284
  def open_experiment(self):
259
- print('ok')
285
+
260
286
  self.browse_experiment_folder()
261
287
  if self.experiment_path_selection.text()!='':
262
288
  self.open_directory()
@@ -120,9 +120,11 @@ class ClassifierWidget(QWidget, Styles):
120
120
  self.property_query_le = QLineEdit()
121
121
  self.property_query_le.setPlaceholderText('classify points using a query such as: area > 100 or eccentricity > 0.95')
122
122
  self.property_query_le.setToolTip('Classify points using a query on measurements.\nYou can use "and" and "or" conditions to combine\nmeasurements (e.g. "area > 100 or eccentricity > 0.95").')
123
+ self.property_query_le.textChanged.connect(self.activate_submit_btn)
123
124
  hbox_classify.addWidget(self.property_query_le, 70)
124
125
  self.submit_query_btn = QPushButton('Submit...')
125
126
  self.submit_query_btn.clicked.connect(self.apply_property_query)
127
+ self.submit_query_btn.setEnabled(False)
126
128
  hbox_classify.addWidget(self.submit_query_btn, 20)
127
129
  layout.addLayout(hbox_classify)
128
130
 
@@ -182,15 +184,24 @@ class ClassifierWidget(QWidget, Styles):
182
184
 
183
185
  layout.addWidget(QLabel())
184
186
 
185
-
186
187
  self.submit_btn = QPushButton('apply')
187
188
  self.submit_btn.setStyleSheet(self.button_style_sheet)
188
189
  self.submit_btn.clicked.connect(self.submit_classification)
190
+ self.submit_btn.setEnabled(False)
189
191
  layout.addWidget(self.submit_btn, 30)
190
192
 
191
193
  self.frame_slider.valueChanged.connect(self.set_frame)
192
194
  self.alpha_slider.valueChanged.connect(self.set_transparency)
193
195
 
196
+ def activate_submit_btn(self):
197
+
198
+ if self.property_query_le.text()=='':
199
+ self.submit_query_btn.setEnabled(False)
200
+ self.submit_btn.setEnabled(False)
201
+ else:
202
+ self.submit_query_btn.setEnabled(True)
203
+ self.submit_btn.setEnabled(True)
204
+
194
205
  def activate_r2(self):
195
206
  if self.irreversible_event_btn.isChecked() and self.time_corr.isChecked():
196
207
  for wg in [self.r2_slider, self.r2_label]:
@@ -250,32 +261,32 @@ class ClassifierWidget(QWidget, Styles):
250
261
  self.scat_props.set_offsets(self.df.loc[self.df['FRAME']==self.currentFrame,[self.features_cb[1].currentText(),self.features_cb[0].currentText()]].to_numpy())
251
262
  colors = [color_from_status(c) for c in self.df.loc[self.df['FRAME']==self.currentFrame,class_name].to_numpy()]
252
263
  self.scat_props.set_facecolor(colors)
253
- self.scat_props.set_alpha(self.currentAlpha)
254
- self.ax_props.set_xlabel(self.features_cb[1].currentText())
255
- self.ax_props.set_ylabel(self.features_cb[0].currentText())
256
264
  else:
257
265
  self.scat_props.set_offsets(self.df[[self.features_cb[1].currentText(),self.features_cb[0].currentText()]].to_numpy())
258
266
  colors = [color_from_status(c) for c in self.df[class_name].to_numpy()]
259
267
  self.scat_props.set_facecolor(colors)
260
- self.scat_props.set_alpha(self.currentAlpha)
268
+
269
+ self.scat_props.set_alpha(self.currentAlpha)
270
+
271
+ if feature_changed:
272
+
261
273
  self.ax_props.set_xlabel(self.features_cb[1].currentText())
262
274
  self.ax_props.set_ylabel(self.features_cb[0].currentText())
263
275
 
264
-
265
- feat_x = self.features_cb[1].currentText()
266
- feat_y = self.features_cb[0].currentText()
267
- min_x = self.df.dropna(subset=feat_x)[feat_x].min()
268
- max_x = self.df.dropna(subset=feat_x)[feat_x].max()
269
- min_y = self.df.dropna(subset=feat_y)[feat_y].min()
270
- max_y = self.df.dropna(subset=feat_y)[feat_y].max()
271
-
272
- if min_x==min_x and max_x==max_x:
273
- self.ax_props.set_xlim(min_x, max_x)
274
- if min_y==min_y and max_y==max_y:
275
- self.ax_props.set_ylim(min_y, max_y)
276
+ feat_x = self.features_cb[1].currentText()
277
+ feat_y = self.features_cb[0].currentText()
278
+ min_x = self.df.dropna(subset=feat_x)[feat_x].min()
279
+ max_x = self.df.dropna(subset=feat_x)[feat_x].max()
280
+ min_y = self.df.dropna(subset=feat_y)[feat_y].min()
281
+ max_y = self.df.dropna(subset=feat_y)[feat_y].max()
282
+
283
+ if min_x==min_x and max_x==max_x:
284
+ self.ax_props.set_xlim(min_x, max_x)
285
+ if min_y==min_y and max_y==max_y:
286
+ self.ax_props.set_ylim(min_y, max_y)
276
287
 
277
- if feature_changed:
278
288
  self.propscanvas.canvas.toolbar.update()
289
+
279
290
  self.propscanvas.canvas.draw_idle()
280
291
 
281
292
  except Exception as e:
@@ -284,7 +295,20 @@ class ClassifierWidget(QWidget, Styles):
284
295
  def apply_property_query(self):
285
296
 
286
297
  query = self.property_query_le.text()
287
- self.df = classify_cells_from_query(self.df, self.name_le.text(), query)
298
+
299
+ try:
300
+ self.df = classify_cells_from_query(self.df, self.name_le.text(), query)
301
+ except Exception as e:
302
+ msgBox = QMessageBox()
303
+ msgBox.setIcon(QMessageBox.Warning)
304
+ msgBox.setText(f"The query could not be understood. No filtering was applied. {e}")
305
+ msgBox.setWindowTitle("Warning")
306
+ msgBox.setStandardButtons(QMessageBox.Ok)
307
+ returnValue = msgBox.exec()
308
+ if returnValue == QMessageBox.Ok:
309
+ self.auto_close = False
310
+ return None
311
+
288
312
  self.class_name = "status_"+self.name_le.text()
289
313
  if self.df is None:
290
314
  msgBox = QMessageBox()
@@ -294,8 +318,10 @@ class ClassifierWidget(QWidget, Styles):
294
318
  msgBox.setStandardButtons(QMessageBox.Ok)
295
319
  returnValue = msgBox.exec()
296
320
  if returnValue == QMessageBox.Ok:
321
+ self.auto_close = False
297
322
  return None
298
- self.update_props_scatter()
323
+
324
+ self.update_props_scatter(feature_changed=False)
299
325
 
300
326
  def set_frame(self, value):
301
327
  xlim=self.ax_props.get_xlim()
@@ -329,12 +355,14 @@ class ClassifierWidget(QWidget, Styles):
329
355
  self.project_times_btn.setIcon(icon(MDI6.math_integral_box,color="black"))
330
356
  self.project_times_btn.setIconSize(QSize(20, 20))
331
357
  self.frame_slider.setEnabled(False)
332
- self.update_props_scatter()
358
+ self.update_props_scatter(feature_changed=False)
333
359
 
334
360
  def submit_classification(self):
335
361
 
336
- print('submit')
362
+ self.auto_close = True
337
363
  self.apply_property_query()
364
+ if not self.auto_close:
365
+ return None
338
366
 
339
367
  if self.time_corr.isChecked():
340
368
  self.class_name_user = 'class_'+self.name_le.text()
@@ -8,7 +8,7 @@ from superqt import QLabeledRangeSlider, QLabeledDoubleSlider, QLabeledSlider, Q
8
8
  from superqt.fonticon import icon
9
9
  from fonticon_mdi6 import MDI6
10
10
  from celldetective.utils import _extract_channel_indices_from_config
11
- from celldetective.gui.viewers import ThresholdedStackVisualizer, CellEdgeVisualizer, StackVisualizer, CellSizeViewer
11
+ from celldetective.gui.viewers import ThresholdedStackVisualizer, CellEdgeVisualizer, StackVisualizer, CellSizeViewer, ChannelOffsetViewer
12
12
  from celldetective.gui import Styles
13
13
  from celldetective.preprocessing import correct_background_model, correct_background_model_free, estimate_background_per_condition
14
14
  from functools import partial
@@ -535,7 +535,6 @@ class BackgroundFitCorrectionLayout(QGridLayout, Styles):
535
535
  self.threshold_viewer_btn.clicked.connect(self.set_threshold_graphically)
536
536
  self.threshold_viewer_btn.setToolTip('Set the threshold graphically.')
537
537
 
538
-
539
538
  self.model_lbl = QLabel('Model: ')
540
539
  self.model_lbl.setToolTip('2D model to fit the background with.')
541
540
  self.models_cb = QComboBox()
@@ -560,6 +559,8 @@ class BackgroundFitCorrectionLayout(QGridLayout, Styles):
560
559
  self.corrected_stack_viewer,
561
560
  self.add_correction_btn
562
561
  ])
562
+
563
+
563
564
  def add_to_layout(self):
564
565
 
565
566
  channel_layout = QHBoxLayout()
@@ -572,6 +573,7 @@ class BackgroundFitCorrectionLayout(QGridLayout, Styles):
572
573
  subthreshold_layout = QHBoxLayout()
573
574
  subthreshold_layout.addWidget(self.threshold_le, 95)
574
575
  subthreshold_layout.addWidget(self.threshold_viewer_btn, 5)
576
+
575
577
  threshold_layout.addLayout(subthreshold_layout, 75)
576
578
  self.addLayout(threshold_layout, 1, 0, 1, 3)
577
579
 
@@ -878,18 +880,26 @@ class ProtocolDesignerLayout(QVBoxLayout, Styles):
878
880
 
879
881
  def generate_layout(self):
880
882
 
883
+ self.correction_layout = QVBoxLayout()
884
+
885
+ self.background_correction_layout = QVBoxLayout()
886
+ self.background_correction_layout.setContentsMargins(0,0,0,0)
881
887
  self.title_layout = QHBoxLayout()
882
888
  self.title_layout.addWidget(self.title_lbl, 100, alignment=Qt.AlignCenter)
889
+ self.background_correction_layout.addLayout(self.title_layout)
890
+ self.background_correction_layout.addWidget(self.tabs)
891
+ self.correction_layout.addLayout(self.background_correction_layout)
892
+
893
+ self.addLayout(self.correction_layout)
883
894
 
884
- self.addLayout(self.title_layout)
885
- self.addWidget(self.tabs)
886
-
895
+ self.list_layout = QVBoxLayout()
887
896
  list_header_layout = QHBoxLayout()
888
897
  list_header_layout.addWidget(self.protocol_list_lbl)
889
898
  list_header_layout.addWidget(self.delete_protocol_btn, alignment=Qt.AlignRight)
890
- self.addLayout(list_header_layout)
899
+ self.list_layout.addLayout(list_header_layout)
900
+ self.list_layout.addWidget(self.protocol_list)
891
901
 
892
- self.addWidget(self.protocol_list)
902
+ self.addLayout(self.list_layout)
893
903
 
894
904
 
895
905
  def remove_protocol_from_list(self):
@@ -899,6 +909,117 @@ class ProtocolDesignerLayout(QVBoxLayout, Styles):
899
909
  del self.protocols[current_item]
900
910
  self.protocol_list.takeItem(current_item)
901
911
 
912
+ class ChannelOffsetOptionsLayout(QVBoxLayout, Styles):
913
+
914
+ def __init__(self, parent_window=None, *args, **kwargs):
915
+
916
+ super().__init__(*args, **kwargs)
917
+
918
+ self.parent_window = parent_window
919
+ if hasattr(self.parent_window.parent_window, 'exp_config'):
920
+ self.attr_parent = self.parent_window.parent_window
921
+ else:
922
+ self.attr_parent = self.parent_window.parent_window.parent_window
923
+
924
+ self.channel_names = self.attr_parent.exp_channels
925
+
926
+ self.setContentsMargins(15,15,15,15)
927
+ self.generate_widgets()
928
+ self.add_to_layout()
929
+
930
+ def generate_widgets(self):
931
+
932
+ self.channel_lbl = QLabel('Channel: ')
933
+ self.channels_cb = QComboBox()
934
+ self.channels_cb.addItems(self.channel_names)
935
+
936
+ self.shift_lbl = QLabel('Shift: ')
937
+ self.shift_h_lbl = QLabel('(h): ')
938
+ self.shift_v_lbl = QLabel('(v): ')
939
+
940
+ self.set_shift_btn = QPushButton()
941
+ self.set_shift_btn.setIcon(icon(MDI6.image_check, color="k"))
942
+ self.set_shift_btn.setStyleSheet(self.button_select_all)
943
+ self.set_shift_btn.setToolTip('Set the channel shift.')
944
+ self.set_shift_btn.clicked.connect(self.open_offset_viewer)
945
+
946
+ self.add_correction_btn = QPushButton('Add correction')
947
+ self.add_correction_btn.setStyleSheet(self.button_style_sheet_2)
948
+ self.add_correction_btn.setIcon(icon(MDI6.plus, color="#1565c0"))
949
+ self.add_correction_btn.setToolTip('Add correction.')
950
+ self.add_correction_btn.setIconSize(QSize(25, 25))
951
+ self.add_correction_btn.clicked.connect(self.add_instructions_to_parent_list)
952
+
953
+ self.vertical_shift_le = ThresholdLineEdit(init_value=0, connected_buttons=[self.add_correction_btn],placeholder='vertical shift [pixels]', value_type='float')
954
+ self.horizontal_shift_le = ThresholdLineEdit(init_value=0, connected_buttons=[self.add_correction_btn],placeholder='vertical shift [pixels]', value_type='float')
955
+
956
+ def add_to_layout(self):
957
+
958
+ channel_ch_hbox = QHBoxLayout()
959
+ channel_ch_hbox.addWidget(self.channel_lbl, 25)
960
+ channel_ch_hbox.addWidget(self.channels_cb, 75)
961
+ self.addLayout(channel_ch_hbox)
962
+
963
+ shift_hbox = QHBoxLayout()
964
+ shift_hbox.addWidget(self.shift_lbl, 25)
965
+
966
+ shift_subhbox = QHBoxLayout()
967
+ shift_subhbox.addWidget(self.shift_h_lbl, 10)
968
+ shift_subhbox.addWidget(self.horizontal_shift_le, 75//2)
969
+ shift_subhbox.addWidget(self.shift_v_lbl, 10)
970
+ shift_subhbox.addWidget(self.vertical_shift_le, 75//2)
971
+ shift_subhbox.addWidget(self.set_shift_btn, 5)
972
+
973
+ shift_hbox.addLayout(shift_subhbox, 75)
974
+ self.addLayout(shift_hbox)
975
+
976
+ btn_hbox = QHBoxLayout()
977
+ btn_hbox.addWidget(self.add_correction_btn, 95)
978
+ self.addLayout(btn_hbox)
979
+
980
+ def add_instructions_to_parent_list(self):
981
+
982
+ self.generate_instructions()
983
+ self.parent_window.protocol_layout.protocols.append(self.instructions)
984
+ correction_description = ""
985
+ for index, (key, value) in enumerate(self.instructions.items()):
986
+ if index > 0:
987
+ correction_description += ", "
988
+ correction_description += str(key) + " : " + str(value)
989
+ self.parent_window.protocol_layout.protocol_list.addItem(correction_description)
990
+
991
+ def generate_instructions(self):
992
+
993
+ self.instructions = {
994
+ "correction_type": "offset",
995
+ "target_channel": self.channels_cb.currentText(),
996
+ "correction_horizontal": self.horizontal_shift_le.get_threshold(),
997
+ "correction_vertical": self.vertical_shift_le.get_threshold(),
998
+ }
999
+
1000
+
1001
+ def set_target_channel(self):
1002
+
1003
+ channel_indices = _extract_channel_indices_from_config(self.attr_parent.exp_config, [self.channels_cb.currentText()])
1004
+ self.target_channel = channel_indices[0]
1005
+
1006
+ def open_offset_viewer(self):
1007
+
1008
+ self.attr_parent.locate_image()
1009
+ self.set_target_channel()
1010
+
1011
+ if self.attr_parent.current_stack is not None:
1012
+ self.viewer = ChannelOffsetViewer(
1013
+ parent_window = self,
1014
+ stack_path=self.attr_parent.current_stack,
1015
+ channel_names=self.attr_parent.exp_channels,
1016
+ n_channels=len(self.channel_names),
1017
+ channel_cb=True,
1018
+ target_channel=self.target_channel,
1019
+ window_title='offset viewer',
1020
+ )
1021
+ self.viewer.show()
1022
+
902
1023
 
903
1024
  class BackgroundModelFreeCorrectionLayout(QGridLayout, Styles):
904
1025
 
@@ -154,7 +154,7 @@ class ConfigMeasurements(QMainWindow, Styles):
154
154
 
155
155
  grid = QGridLayout(self.iso_frame)
156
156
 
157
- self.iso_lbl = QLabel("ISOTROPIC MEASUREMENTS")
157
+ self.iso_lbl = QLabel("Position-based measurements".upper())
158
158
  self.iso_lbl.setStyleSheet("""
159
159
  font-weight: bold;
160
160
  padding: 0px;
@@ -171,7 +171,7 @@ class ConfigMeasurements(QMainWindow, Styles):
171
171
 
172
172
  grid = QGridLayout(self.features_frame)
173
173
 
174
- self.feature_lbl = QLabel("FEATURES")
174
+ self.feature_lbl = QLabel("Mask-based measurements".upper())
175
175
  self.feature_lbl.setStyleSheet("""
176
176
  font-weight: bold;
177
177
  padding: 0px;
@@ -262,7 +262,7 @@ class ConfigMeasurements(QMainWindow, Styles):
262
262
  self.add_feature_btn.setToolTip("Add feature")
263
263
  self.add_feature_btn.setIconSize(QSize(20, 20))
264
264
 
265
- self.features_list = ListWidget(FeatureChoice, initial_features=['area', 'intensity_mean', ])
265
+ self.features_list = ListWidget(FeatureChoice, initial_features=['area', 'intensity_nanmean', ])
266
266
 
267
267
  self.del_feature_btn.clicked.connect(self.features_list.removeSel)
268
268
  self.add_feature_btn.clicked.connect(self.features_list.addItem)