celldetective 1.3.9.post4__py3-none-any.whl → 1.4.0__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.
Files changed (57) hide show
  1. celldetective/__init__.py +0 -3
  2. celldetective/_version.py +1 -1
  3. celldetective/events.py +2 -4
  4. celldetective/extra_properties.py +320 -24
  5. celldetective/gui/InitWindow.py +33 -45
  6. celldetective/gui/__init__.py +1 -0
  7. celldetective/gui/about.py +19 -15
  8. celldetective/gui/analyze_block.py +34 -19
  9. celldetective/gui/base_components.py +23 -0
  10. celldetective/gui/btrack_options.py +26 -34
  11. celldetective/gui/classifier_widget.py +71 -80
  12. celldetective/gui/configure_new_exp.py +113 -17
  13. celldetective/gui/control_panel.py +68 -141
  14. celldetective/gui/generic_signal_plot.py +9 -12
  15. celldetective/gui/gui_utils.py +49 -21
  16. celldetective/gui/json_readers.py +5 -4
  17. celldetective/gui/layouts.py +246 -22
  18. celldetective/gui/measurement_options.py +32 -17
  19. celldetective/gui/neighborhood_options.py +10 -13
  20. celldetective/gui/plot_measurements.py +21 -17
  21. celldetective/gui/plot_signals_ui.py +131 -75
  22. celldetective/gui/process_block.py +180 -123
  23. celldetective/gui/processes/compute_neighborhood.py +594 -0
  24. celldetective/gui/processes/measure_cells.py +5 -0
  25. celldetective/gui/processes/segment_cells.py +27 -6
  26. celldetective/gui/processes/track_cells.py +6 -0
  27. celldetective/gui/retrain_segmentation_model_options.py +12 -20
  28. celldetective/gui/retrain_signal_model_options.py +57 -56
  29. celldetective/gui/seg_model_loader.py +21 -62
  30. celldetective/gui/signal_annotator.py +139 -72
  31. celldetective/gui/signal_annotator2.py +431 -635
  32. celldetective/gui/signal_annotator_options.py +8 -11
  33. celldetective/gui/survival_ui.py +49 -95
  34. celldetective/gui/tableUI.py +28 -25
  35. celldetective/gui/thresholds_gui.py +617 -1221
  36. celldetective/gui/viewers.py +106 -39
  37. celldetective/gui/workers.py +9 -3
  38. celldetective/io.py +73 -27
  39. celldetective/measure.py +63 -27
  40. celldetective/neighborhood.py +342 -268
  41. celldetective/preprocessing.py +25 -17
  42. celldetective/relative_measurements.py +50 -29
  43. celldetective/scripts/analyze_signals.py +4 -1
  44. celldetective/scripts/measure_relative.py +4 -1
  45. celldetective/scripts/segment_cells.py +0 -6
  46. celldetective/scripts/track_cells.py +3 -1
  47. celldetective/scripts/train_segmentation_model.py +7 -4
  48. celldetective/signals.py +29 -14
  49. celldetective/tracking.py +7 -2
  50. celldetective/utils.py +36 -8
  51. {celldetective-1.3.9.post4.dist-info → celldetective-1.4.0.dist-info}/METADATA +24 -16
  52. {celldetective-1.3.9.post4.dist-info → celldetective-1.4.0.dist-info}/RECORD +57 -55
  53. {celldetective-1.3.9.post4.dist-info → celldetective-1.4.0.dist-info}/WHEEL +1 -1
  54. tests/test_qt.py +21 -21
  55. {celldetective-1.3.9.post4.dist-info → celldetective-1.4.0.dist-info}/entry_points.txt +0 -0
  56. {celldetective-1.3.9.post4.dist-info → celldetective-1.4.0.dist-info/licenses}/LICENSE +0 -0
  57. {celldetective-1.3.9.post4.dist-info → celldetective-1.4.0.dist-info}/top_level.txt +0 -0
celldetective/__init__.py CHANGED
@@ -1,4 +1 @@
1
1
  from ._version import __version__
2
- #from .tracking import track, clean_trajectories
3
- #from .measure import contour_of_instance_segmentation
4
-
celldetective/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.3.9.post4"
1
+ __version__ = "1.4.0"
celldetective/events.py CHANGED
@@ -52,7 +52,7 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
52
52
  >>> event_times = [5, 10, 15]
53
53
  >>> max_times = [20, 20, 20]
54
54
  >>> origin_times = [0, 0, 5]
55
- >>> events, survival_times = switch_to_events_v2(classes, event_times, max_times, origin_times, FrameToMin=0.5)
55
+ >>> events, survival_times = switch_to_events(classes, event_times, max_times, origin_times, FrameToMin=0.5)
56
56
  # This would process the events considering left censorship and convert survival times to minutes.
57
57
 
58
58
  """
@@ -189,6 +189,7 @@ def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMi
189
189
  assert class_of_interest in cols,"The requested class cannot be found in the dataframe..."
190
190
  assert t_event in cols,"The event time cannot be found in the dataframe..."
191
191
  left_censored = False
192
+ first_detections = None
192
193
 
193
194
  if not pairs:
194
195
  groupby_cols = ['position','TRACK_ID']
@@ -209,10 +210,7 @@ def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMi
209
210
  assert t_reference in cols,"The reference time cannot be found in the dataframe..."
210
211
  first_detections = df.groupby(groupby_cols)[t_reference].max().values
211
212
 
212
-
213
- print(f"{classes=} {event_times=} {max_times=} {first_detections=}")
214
213
  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)
215
- print(f"{events=} {survival_times=}")
216
214
 
217
215
  ks = KaplanMeierFitter()
218
216
  if len(events)>0:
@@ -1,23 +1,45 @@
1
1
  """
2
- Copyright © 2022 Laboratoire Adhesion et Inflammation, Authored by Remy Torro,
3
- Ksenija Dervanova.
4
-
5
- Add extra properties to measure using regionprops (measure_features).
6
- The functions must take "regionmask" as the first argument, corresponding
7
- to the binary mask of each cell of interest generated by regionprops
8
- and an optional "intensity_image" second argument, i.e. the associated
9
- multichannel image crop. The measurement will be named after the function
10
- name. If a function function() has several outputs (e.g. due to a multichannel image),
11
- they will be labelled with an index as function-0, function-1... A routine
12
- automatically replacements keywords "intensity-0", "intensity-1" and so on,
13
- with the actual name of the intensity channel (e.g. "intensity-0" ->
14
- "brightfield_channel"). Due to this indexing behavior, avoid using digits
15
- in the function names and prefer text instead: "intensity-99()" -->
16
- "intensity-ninety-nine()".
17
-
18
- New functions appear automatically in the ConfigMeasurements widget of the
19
- GUI.
20
-
2
+ Copyright © 2022 Laboratoire Adhesion et Inflammation
3
+ Authored by R. Torro, K. Dervanova, L. Limozin
4
+
5
+ This module defines additional measurement functions for use with `regionprops` via `measure_features`.
6
+
7
+ Usage
8
+ -----
9
+ Each function must follow these conventions:
10
+
11
+ - **First argument:** `regionmask` (numpy array)
12
+ A binary mask of the cell of interest, as provided by `regionprops`.
13
+ - **Optional second argument:** `intensity_image` (numpy array)
14
+ An image crop/bounding box associated with the cell (single-channel at a time).
15
+
16
+ Unlike the default `regionprops` from `scikit-image`, the cell image is **not** masked with zeros outside its boundaries.
17
+ This allows thresholding techniques to be used in measurements.
18
+
19
+ Naming Conventions & Indexing
20
+ ------------------------------
21
+ - The measurement name is derived from the function name.
22
+ - If a function returns multiple values (e.g., for multichannel images), outputs are labeled sequentially:
23
+ `function-0`, `function-1`, etc.
24
+ - To rename these outputs, use `rename_intensity_column` from `celldetective.utils`.
25
+ - `"intensity"` in function names is automatically replaced with the actual channel name:
26
+ - Example: `"intensity-0"` → `"brightfield_channel"`.
27
+ - **Avoid digits smaller than the number of channels in function names** to prevent indexing conflicts.
28
+ Prefer text-based names instead:
29
+
30
+ .. code-block:: python
31
+
32
+ # Bad practice:
33
+ def intensity2(regionmask, intensity_image):
34
+ pass
35
+
36
+ # Recommended:
37
+ def intensity_two(regionmask, intensity_image):
38
+ pass
39
+
40
+ GUI Integration
41
+ ---------------
42
+ New functions are **automatically** added to the list of available measurements in the graphical interface.
21
43
  """
22
44
 
23
45
  import warnings
@@ -97,6 +119,45 @@ def fraction_of_area_detected_in_intensity(regionmask, intensity_image, target_c
97
119
 
98
120
  def area_detected_in_intensity(regionmask, intensity_image, target_channel='adhesion_channel'):
99
121
 
122
+ """
123
+ Computes the detected area within the regionmask based on threshold-based segmentation.
124
+
125
+ The function applies a predefined filtering and thresholding pipeline to the intensity image (normalized adhesion channel)
126
+ to detect significant regions. The resulting segmented regions are restricted to the
127
+ `regionmask`, ensuring that only the relevant area is measured.
128
+
129
+ Parameters
130
+ ----------
131
+ regionmask : ndarray
132
+ A binary mask (2D array) where nonzero values define the region of interest.
133
+ intensity_image : ndarray
134
+ A 2D array of the same shape as `regionmask`, representing the intensity
135
+ values associated with the region.
136
+ target_channel : str, optional
137
+ Name of the intensity channel used for measurement. Defaults to `'adhesion_channel'`.
138
+
139
+ Returns
140
+ -------
141
+ detected_area : float
142
+ The total area (number of pixels) detected based on intensity-based segmentation.
143
+
144
+ Notes
145
+ -----
146
+ - The segmentation is performed using `segment_frame_from_thresholds()` with predefined parameters:
147
+
148
+ - Thresholding range: `[0.02, 1000]`
149
+ - Filters applied in sequence:
150
+
151
+ - `"subtract"` with value `1` (subtract 1 from intensity values)
152
+ - `"abs"` (take absolute value of intensities)
153
+ - `"gauss"` with sigma `0.8` (apply Gauss filter with sigma `0.8`)
154
+
155
+ - The segmentation includes hole filling.
156
+ - The detected regions are converted to a binary mask (`lbl > 0`).
157
+ - Any pixels outside the `regionmask` are excluded from the measurement.
158
+
159
+ """
160
+
100
161
  instructions = {
101
162
  "thresholds": [
102
163
  0.02,
@@ -125,9 +186,46 @@ def area_detected_in_intensity(regionmask, intensity_image, target_channel='adhe
125
186
  return float(np.sum(lbl))
126
187
 
127
188
 
128
- def area_dark_intensity(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True): #, target_channel='adhesion_channel'
189
+ def area_dark_intensity(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True, threshold=0.95): #, target_channel='adhesion_channel'
129
190
 
130
- subregion = (intensity_image < 0.95)*regionmask # under one, under 0.8, under 0.6, whatever value!
191
+ """
192
+ Computes the absolute area within the regionmask where the intensity is below a given threshold.
193
+
194
+ This function identifies pixels in the region where the intensity is lower than `threshold`.
195
+ If `fill_holes` is `True`, small enclosed holes in the detected dark regions are filled before
196
+ computing the total area.
197
+
198
+ Parameters
199
+ ----------
200
+ regionmask : ndarray
201
+ A binary mask (2D array) where nonzero values define the region of interest.
202
+ intensity_image : ndarray
203
+ A 2D array of the same shape as `regionmask`, representing the intensity
204
+ values associated with the region.
205
+ target_channel : str, optional
206
+ Name of the intensity channel used for measurement. Defaults to `'adhesion_channel'`.
207
+ fill_holes : bool, optional
208
+ If `True`, fills enclosed holes in the detected dark intensity regions before computing
209
+ the area. Defaults to `True`.
210
+ threshold : float, optional
211
+ Intensity threshold below which a pixel is considered part of a dark region.
212
+ Defaults to `0.95`.
213
+
214
+ Returns
215
+ -------
216
+ dark_area : float
217
+ The absolute area (number of pixels) where intensity values are below `threshold`, within the regionmask.
218
+
219
+ Notes
220
+ -----
221
+ - The default threshold for defining "dark" intensity regions is `0.95`, but it can be adjusted.
222
+ - If `fill_holes` is `True`, the function applies hole-filling to the detected dark regions
223
+ using `skimage.measure.label` and `fill_label_holes()`.
224
+ - The `target_channel` parameter tells regionprops to only measure this channel.
225
+
226
+ """
227
+
228
+ subregion = (intensity_image < threshold)*regionmask # under one, under 0.8, under 0.6, whatever value!
131
229
  if fill_holes:
132
230
  subregion = skm.label(subregion, connectivity=2, background=0)
133
231
  subregion = fill_label_holes(subregion)
@@ -136,9 +234,9 @@ def area_dark_intensity(regionmask, intensity_image, target_channel='adhesion_ch
136
234
  return float(np.sum(subregion))
137
235
 
138
236
 
139
- def fraction_of_area_dark_intensity(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True): #, target_channel='adhesion_channel'
237
+ def fraction_of_area_dark_intensity(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True, threshold=0.95): #, target_channel='adhesion_channel'
140
238
 
141
- subregion = (intensity_image < 0.95)*regionmask # under one, under 0.8, under 0.6, whatever value!
239
+ subregion = (intensity_image < threshold)*regionmask # under one, under 0.8, under 0.6, whatever value!
142
240
  if fill_holes:
143
241
  subregion = skm.label(subregion, connectivity=2, background=0)
144
242
  subregion = fill_label_holes(subregion)
@@ -147,6 +245,138 @@ def fraction_of_area_dark_intensity(regionmask, intensity_image, target_channel=
147
245
  return float(np.sum(subregion)) / float(np.sum(regionmask))
148
246
 
149
247
 
248
+ def area_dark_intensity_nintyfive(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True): #, target_channel='adhesion_channel'
249
+
250
+ subregion = (intensity_image < 0.95)*regionmask # under one, under 0.8, under 0.6, whatever value!
251
+ if fill_holes:
252
+ subregion = skm.label(subregion, connectivity=2, background=0)
253
+ subregion = fill_label_holes(subregion)
254
+ subregion[subregion>0] = 1
255
+
256
+ return float(np.sum(subregion))
257
+
258
+ def area_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True): #, target_channel='adhesion_channel'
259
+
260
+ subregion = (intensity_image < 0.90)*regionmask # under one, under 0.8, under 0.6, whatever value!
261
+ if fill_holes:
262
+ subregion = skm.label(subregion, connectivity=2, background=0)
263
+ subregion = fill_label_holes(subregion)
264
+ subregion[subregion>0] = 1
265
+
266
+ return float(np.sum(subregion))
267
+
268
+ def mean_dark_intensity_nintyfive(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True):
269
+ """
270
+ Calculate the mean intensity in a dark subregion below 95, handling NaN values.
271
+
272
+ """
273
+ subregion = (intensity_image < 0.95) * regionmask
274
+
275
+ if fill_holes:
276
+ subregion = skm.label(subregion, connectivity=2, background=0)
277
+ subregion = fill_label_holes(subregion)
278
+ subregion[subregion > 0] = 1
279
+
280
+
281
+ masked_intensity = intensity_image[subregion == 1]
282
+
283
+ return float(np.nanmean(masked_intensity))
284
+
285
+
286
+ def mean_dark_intensity_nintyfive_fillhole_false(regionmask, intensity_image, target_channel='adhesion_channel'):
287
+ """
288
+ Calculate the mean intensity in a dark subregion below 95, handling NaN values.
289
+ """
290
+ subregion = (intensity_image < 0.95) * regionmask # Select dark regions within the mask
291
+
292
+ masked_intensity = intensity_image[subregion == 1] # Extract pixel values from the selected region
293
+
294
+ return float(np.nanmean(masked_intensity)) # Compute mean, ignoring NaNs
295
+
296
+ def mean_dark_intensity_ninty_fillhole_false(regionmask, intensity_image, target_channel='adhesion_channel'):
297
+ """
298
+ Calculate the mean intensity in a dark subregion, handling NaN values.
299
+ """
300
+ subregion = (intensity_image < 0.90) * regionmask # Select dark regions within the mask
301
+
302
+ masked_intensity = intensity_image[subregion == 1] # Extract pixel values from the selected region
303
+
304
+ return float(np.nanmean(masked_intensity)) # Compute mean, ignoring NaNs
305
+
306
+
307
+ def mean_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True):
308
+ """
309
+ Calculate the mean intensity in a dark subregion below 90, handling NaN values.
310
+
311
+ """
312
+ subregion = (intensity_image < 0.90) * regionmask
313
+
314
+ if fill_holes:
315
+ subregion = skm.label(subregion, connectivity=2, background=0)
316
+ subregion = fill_label_holes(subregion)
317
+ subregion[subregion > 0] = 1
318
+
319
+
320
+ masked_intensity = intensity_image[subregion == 1]
321
+
322
+ return float(np.nanmean(masked_intensity))
323
+
324
+ def mean_dark_intensity_eight_five(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True):
325
+ """
326
+ Calculate the mean intensity in a dark subregion below 85, handling NaN values.
327
+
328
+ """
329
+ subregion = (intensity_image < 0.85) * regionmask
330
+
331
+ if fill_holes:
332
+ subregion = skm.label(subregion, connectivity=2, background=0)
333
+ subregion = fill_label_holes(subregion)
334
+ subregion[subregion > 0] = 1
335
+
336
+
337
+ masked_intensity = intensity_image[subregion == 1]
338
+
339
+ return float(np.nanmean(masked_intensity))
340
+
341
+
342
+ def mean_dark_intensity_eight_five_fillhole_false(regionmask, intensity_image, target_channel='adhesion_channel'):
343
+
344
+ subregion = (intensity_image < 0.85) * regionmask # Select dark regions within the mask
345
+
346
+ masked_intensity = intensity_image[subregion == 1] # Extract pixel values from the selected region
347
+
348
+ return float(np.nanmean(masked_intensity)) # Compute mean, ignoring NaNs
349
+
350
+ def percentile_zero_one_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
351
+
352
+ subregion = (intensity_image < 0.95) * regionmask
353
+ return float(np.nanpercentile(intensity_image[subregion],0.1))
354
+
355
+
356
+ def percentile_one_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
357
+
358
+ subregion = (intensity_image < 0.95) * regionmask
359
+ return float(np.nanpercentile(intensity_image[subregion],1))
360
+
361
+
362
+ def percentile_five_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
363
+
364
+ subregion = (intensity_image < 0.95) * regionmask
365
+ return float(np.nanpercentile(intensity_image[subregion],5))
366
+
367
+
368
+ def percentile_ten_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
369
+
370
+ subregion = (intensity_image < 0.95) * regionmask
371
+ return float(np.nanpercentile(intensity_image[subregion],10))
372
+
373
+
374
+ def percentile_ninty_five_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
375
+
376
+ subregion = (intensity_image < 0.95) * regionmask
377
+ return float(np.nanpercentile(intensity_image[subregion],95))
378
+
379
+
150
380
  def intensity_percentile_ninety_nine(regionmask, intensity_image):
151
381
  return np.nanpercentile(intensity_image[regionmask],99)
152
382
 
@@ -183,6 +413,38 @@ def intensity_nanmean(regionmask, intensity_image):
183
413
 
184
414
  def intensity_center_of_mass_displacement(regionmask, intensity_image):
185
415
 
416
+ """
417
+ Computes the displacement between the geometric centroid and the
418
+ intensity-weighted center of mass of a region.
419
+
420
+ Parameters
421
+ ----------
422
+ regionmask : ndarray
423
+ A binary mask (2D array) where nonzero values indicate the region of interest.
424
+ intensity_image : ndarray
425
+ A 2D array of the same shape as `regionmask`, representing the intensity
426
+ values associated with the region.
427
+
428
+ Returns
429
+ -------
430
+ distance : float
431
+ Euclidean distance between the geometric centroid and the intensity-weighted center of mass.
432
+ direction_arctan : float
433
+ Angle (in degrees) of displacement from the geometric centroid to the intensity-weighted center of mass,
434
+ computed using `arctan2(delta_y, delta_x)`.
435
+ delta_x : float
436
+ Difference in x-coordinates (intensity-weighted centroid - geometric centroid).
437
+ delta_y : float
438
+ Difference in y-coordinates (intensity-weighted centroid - geometric centroid).
439
+
440
+ Notes
441
+ -----
442
+ - If the `intensity_image` contains NaN values, it is first processed using `interpolate_nan()`.
443
+ - Negative intensity values are set to zero to prevent misbehavior in center of mass calculation.
444
+ - If the intensity image is entirely zero, all outputs are `NaN`.
445
+
446
+ """
447
+
186
448
  if np.any(intensity_image!=intensity_image):
187
449
  intensity_image = interpolate_nan(intensity_image.copy())
188
450
 
@@ -247,7 +509,41 @@ def intensity_center_of_mass_displacement_edge(regionmask, intensity_image):
247
509
  def intensity_radial_gradient(regionmask, intensity_image):
248
510
 
249
511
  """
250
- Determine if intensities are following a linear gradient from center to edge of the cell.
512
+ Determines whether the intensity follows a radial gradient from the center to the edge of the cell.
513
+
514
+ The function fits a linear model to the intensity values as a function of distance from the center
515
+ (computed via the Euclidean distance transform). The slope of the fitted line indicates whether
516
+ the intensity is higher at the center or at the edges.
517
+
518
+ Parameters
519
+ ----------
520
+ regionmask : ndarray
521
+ A binary mask (2D array) where nonzero values define the region of interest.
522
+ intensity_image : ndarray
523
+ A 2D array of the same shape as `regionmask`, representing the intensity
524
+ values associated with the region.
525
+
526
+ Returns
527
+ -------
528
+ slope : float
529
+ Slope of the fitted linear model.
530
+
531
+ - If `slope > 0`: Intensity increases towards the edge.
532
+ - If `slope < 0`: Intensity is higher at the center.
533
+
534
+ intercept : float
535
+ Intercept of the fitted linear model.
536
+ r2 : float
537
+ Coefficient of determination (R²), indicating how well the linear model fits the intensity profile.
538
+
539
+ Notes
540
+ -----
541
+ - If the `intensity_image` contains NaN values, they are interpolated using `interpolate_nan()`.
542
+ - The Euclidean distance transform (`distance_transform_edt`) is used to compute the distance
543
+ of each pixel from the edge.
544
+ - The x-values for the linear fit are reversed so that the origin is at the center.
545
+ - A warning suppression is applied to ignore messages about poorly conditioned polynomial fits.
546
+
251
547
  """
252
548
 
253
549
  if np.any(intensity_image!=intensity_image):
@@ -1,29 +1,27 @@
1
+ import gc
2
+ import json
1
3
  import os
2
-
3
- from PyQt5.QtWidgets import QApplication, QMainWindow
4
- from PyQt5.QtWidgets import QFileDialog, QDialog, QWidget, QVBoxLayout, QCheckBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QMenu, QAction
5
- from PyQt5.QtCore import Qt, QUrl
6
- from PyQt5.QtGui import QIcon, QDesktopServices, QIntValidator
7
-
8
4
  from glob import glob
9
- from superqt.fonticon import icon
10
- from fonticon_mdi6 import MDI6
11
-
12
- from celldetective.gui.about import AboutWidget
13
- from celldetective.io import correct_annotation
14
- from celldetective.utils import download_zenodo_file
15
- from celldetective.gui.gui_utils import center_window
16
- from celldetective.gui import Styles, ControlPanel, ConfigNewExperiment
5
+ from subprocess import Popen, check_output
17
6
 
18
- import gc
19
- from subprocess import check_output, Popen
7
+ from PyQt5.QtCore import QUrl, Qt
8
+ from PyQt5.QtGui import QDesktopServices, QIntValidator
9
+ from PyQt5.QtWidgets import QAction, QApplication, QCheckBox, QDialog, QFileDialog, QHBoxLayout, QLabel, QLineEdit, \
10
+ QMenu, QPushButton, QVBoxLayout
11
+ from fonticon_mdi6 import MDI6
20
12
  from psutil import cpu_count
21
- import json
13
+ from superqt.fonticon import icon
22
14
 
15
+ from celldetective.gui import ConfigNewExperiment, ControlPanel, CelldetectiveMainWindow, CelldetectiveWidget
16
+ from celldetective.gui.about import AboutWidget
17
+ from celldetective.gui.gui_utils import center_window, generic_message
23
18
  from celldetective.gui.processes.downloader import DownloadProcess
24
19
  from celldetective.gui.workers import ProgressWindow
20
+ from celldetective.io import correct_annotation, extract_well_name_and_number
21
+ from celldetective.utils import download_zenodo_file, pretty_table
25
22
 
26
- class AppInitWindow(QMainWindow, Styles):
23
+
24
+ class AppInitWindow(CelldetectiveMainWindow):
27
25
 
28
26
  """
29
27
  Initial window to set the experiment folder or create a new one.
@@ -36,7 +34,7 @@ class AppInitWindow(QMainWindow, Styles):
36
34
  self.parent_window = parent_window
37
35
  self.setWindowTitle("celldetective")
38
36
 
39
- self.n_threads = min([1,cpu_count()])
37
+ self.n_threads = min([1, cpu_count()])
40
38
 
41
39
  try:
42
40
  check_output('nvidia-smi')
@@ -48,7 +46,6 @@ class AppInitWindow(QMainWindow, Styles):
48
46
 
49
47
  self.soft_path = software_location
50
48
  self.onlyInt = QIntValidator()
51
- self.setWindowIcon(QIcon(os.sep.join([self.soft_path,'celldetective','icons','logo.png'])))
52
49
 
53
50
  self._createActions()
54
51
  self._createMenuBar()
@@ -58,9 +55,9 @@ class AppInitWindow(QMainWindow, Styles):
58
55
  self.geometry = self.screen.availableGeometry()
59
56
  self.screen_width, self.screen_height = self.geometry.getRect()[-2:]
60
57
 
61
- central_widget = QWidget()
58
+ central_widget = CelldetectiveWidget()
62
59
  self.vertical_layout = QVBoxLayout(central_widget)
63
- self.vertical_layout.setContentsMargins(15,15,15,15)
60
+ self.vertical_layout.setContentsMargins(15, 15, 15, 15)
64
61
  self.vertical_layout.addWidget(QLabel("Experiment folder:"))
65
62
  self.create_locate_exp_hbox()
66
63
  self.create_buttons_hbox()
@@ -79,7 +76,7 @@ class AppInitWindow(QMainWindow, Styles):
79
76
  def create_locate_exp_hbox(self):
80
77
 
81
78
  self.locate_exp_layout = QHBoxLayout()
82
- self.locate_exp_layout.setContentsMargins(0,5,0,0)
79
+ self.locate_exp_layout.setContentsMargins(0, 5, 0, 0)
83
80
  self.experiment_path_selection = QLineEdit()
84
81
  self.experiment_path_selection.setAlignment(Qt.AlignLeft)
85
82
  self.experiment_path_selection.setEnabled(True)
@@ -266,7 +263,7 @@ class AppInitWindow(QMainWindow, Styles):
266
263
 
267
264
  print('setting memory and threads')
268
265
 
269
- self.ThreadsWidget = QWidget()
266
+ self.ThreadsWidget = CelldetectiveWidget()
270
267
  self.ThreadsWidget.setWindowTitle("Threads")
271
268
  layout = QVBoxLayout()
272
269
  self.ThreadsWidget.setLayout(layout)
@@ -382,24 +379,21 @@ class AppInitWindow(QMainWindow, Styles):
382
379
  wells = glob(os.sep.join([self.exp_dir,"W*"]))
383
380
  self.number_of_wells = len(wells)
384
381
  if self.number_of_wells==0:
385
- msgBox = QMessageBox()
386
- msgBox.setIcon(QMessageBox.Critical)
387
- msgBox.setText("No well was found in the experiment folder.\nPlease respect the W*/ nomenclature...")
388
- msgBox.setWindowTitle("Error")
389
- msgBox.setStandardButtons(QMessageBox.Ok)
390
- returnValue = msgBox.exec()
391
- if returnValue == QMessageBox.Ok:
392
- return None
382
+ generic_message("No well was found in the experiment folder.\nPlease respect the W*/ nomenclature...", msg_type="critical")
383
+ return None
393
384
  else:
394
385
  if self.number_of_wells==1:
395
386
  print(f"Found {self.number_of_wells} well...")
396
387
  elif self.number_of_wells>1:
397
388
  print(f"Found {self.number_of_wells} wells...")
398
- number_pos = []
389
+
390
+ number_pos = {}
399
391
  for w in wells:
400
- position_folders = glob(os.sep.join([w,f"{w.split(os.sep)[-1][1]}*", os.sep]))
401
- number_pos.append(len(position_folders))
402
- print(f"Number of positions per well: {number_pos}")
392
+ well_name, well_nbr = extract_well_name_and_number(w)
393
+ position_folders = glob(os.sep.join([w,f"{well_nbr}*", os.sep]))
394
+ number_pos.update({well_name: len(position_folders)})
395
+ print(f"Number of positions per well:")
396
+ pretty_table(number_pos)
403
397
 
404
398
  with open(os.sep.join([self.soft_path,'celldetective','recent.txt']), 'a+') as f:
405
399
  f.write(self.exp_dir+'\n')
@@ -423,12 +417,6 @@ class AppInitWindow(QMainWindow, Styles):
423
417
  else:
424
418
  return None
425
419
  if not os.path.exists(os.sep.join([self.foldername,"config.ini"])):
426
- msgBox = QMessageBox()
427
- msgBox.setIcon(QMessageBox.Warning)
428
- msgBox.setText("No configuration can be found in the selected folder...")
429
- msgBox.setWindowTitle("Warning")
430
- msgBox.setStandardButtons(QMessageBox.Ok)
431
- returnValue = msgBox.exec()
432
- if returnValue == QMessageBox.Ok:
433
- self.experiment_path_selection.setText('')
434
- return None
420
+ generic_message("No configuration can be found in the selected folder...", msg_type="warning")
421
+ self.experiment_path_selection.setText('')
422
+ return None
@@ -1,4 +1,5 @@
1
1
  from .styles import Styles
2
+ from .base_components import CelldetectiveWidget, CelldetectiveMainWindow
2
3
  from .btrack_options import ConfigTracking
3
4
  from .json_readers import ConfigEditor
4
5
  from .tableUI import TableUI
@@ -1,44 +1,48 @@
1
- from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
1
+ from PyQt5.QtWidgets import QVBoxLayout, QLabel
2
2
  from PyQt5.QtGui import QPixmap
3
3
  from PyQt5.QtCore import Qt
4
+
5
+ from celldetective.gui import CelldetectiveWidget
4
6
  from celldetective.utils import get_software_location
5
7
  import os
6
8
  from celldetective.gui.gui_utils import center_window
7
9
  from celldetective._version import __version__
8
10
 
9
- class AboutWidget(QWidget):
10
11
 
12
+ class AboutWidget(CelldetectiveWidget):
13
+
11
14
  def __init__(self):
12
-
13
15
  super().__init__()
14
16
  self.setWindowTitle("About celldetective")
15
- self.setMinimumWidth(300)
17
+ self.setMaximumWidth(320)
16
18
  center_window(self)
17
- logo = QPixmap(os.sep.join([get_software_location(),'celldetective','icons','logo.png']))
18
-
19
+
20
+ logo = QPixmap(os.sep.join([get_software_location(), 'celldetective', 'icons', 'logo.png']))
21
+
19
22
  # Create the layout
20
23
  layout = QVBoxLayout(self)
21
24
  img_label = QLabel('')
22
25
  img_label.setPixmap(logo)
23
26
  layout.addWidget(img_label, alignment=Qt.AlignCenter)
24
-
27
+
25
28
  self.soft_name = QLabel('celldetective')
26
29
  self.soft_name.setStyleSheet("""font-weight: bold;
27
30
  font-size: 18px;
28
31
  """)
29
32
  layout.addWidget(self.soft_name, alignment=Qt.AlignCenter)
30
-
31
- self.version_lbl = QLabel(f"Version {__version__} <a href=\"https://github.com/remyeltorro/celldetective/releases\">(release notes)</a>")
33
+
34
+ self.version_lbl = QLabel(f"Version {__version__} <a href=\"https://github.com/remyeltorro/celldetective"
35
+ f"/releases\">(release notes)</a>")
32
36
  self.version_lbl.setOpenExternalLinks(True)
33
37
  layout.addWidget(self.version_lbl, alignment=Qt.AlignCenter)
34
-
38
+
35
39
  self.lab_lbl = QLabel("Developed at Laboratoire Adhésion et Inflammation (LAI) INSERM U1067 CNRS UMR 7333")
36
40
  self.lab_lbl.setWordWrap(True)
37
41
  layout.addWidget(self.lab_lbl, alignment=Qt.AlignCenter)
38
-
39
- self.centuri_mention = QLabel("The project leading to this publication has received funding from France 2030, the French Government program managed by the French National Research Agency (ANR-16-CONV-0001) and from Excellence Initiative of Aix-Marseille University - A*MIDEX')")
42
+
43
+ self.centuri_mention = QLabel(
44
+ "The project leading to this publication has received funding from France 2030, the French Government "
45
+ "program managed by the French National Research Agency (ANR-16-CONV-0001) and from Excellence Initiative "
46
+ "of Aix-Marseille University - A*MIDEX')")
40
47
  self.centuri_mention.setWordWrap(True)
41
48
  layout.addWidget(self.centuri_mention, alignment=Qt.AlignCenter)
42
-
43
-
44
-