celldetective 1.5.0b0__py3-none-any.whl → 1.5.0b2__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 (29) hide show
  1. celldetective/_version.py +1 -1
  2. celldetective/gui/InitWindow.py +26 -4
  3. celldetective/gui/base/components.py +20 -1
  4. celldetective/gui/base_annotator.py +11 -7
  5. celldetective/gui/event_annotator.py +51 -1060
  6. celldetective/gui/interactions_block.py +55 -25
  7. celldetective/gui/interactive_timeseries_viewer.py +11 -1
  8. celldetective/gui/measure_annotator.py +968 -0
  9. celldetective/gui/process_block.py +88 -34
  10. celldetective/gui/viewers/base_viewer.py +134 -3
  11. celldetective/gui/viewers/contour_viewer.py +4 -4
  12. celldetective/gui/workers.py +124 -10
  13. celldetective/measure.py +3 -0
  14. celldetective/napari/utils.py +29 -19
  15. celldetective/processes/downloader.py +122 -96
  16. celldetective/processes/load_table.py +55 -0
  17. celldetective/processes/measure_cells.py +107 -81
  18. celldetective/processes/track_cells.py +39 -39
  19. celldetective/segmentation.py +1 -1
  20. celldetective/tracking.py +9 -0
  21. celldetective/utils/data_loaders.py +21 -1
  22. celldetective/utils/downloaders.py +38 -0
  23. celldetective/utils/maths.py +14 -1
  24. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/METADATA +1 -1
  25. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/RECORD +29 -27
  26. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/WHEEL +0 -0
  27. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/entry_points.txt +0 -0
  28. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/licenses/LICENSE +0 -0
  29. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/top_level.txt +0 -0
@@ -27,6 +27,7 @@ from celldetective.utils.experiment import (
27
27
  )
28
28
  from celldetective.utils.parsing import config_section_to_dict
29
29
  from celldetective import get_logger
30
+ from celldetective.gui.base.styles import Styles
30
31
 
31
32
  logger = get_logger()
32
33
 
@@ -87,10 +88,16 @@ def control_tracks(
87
88
  position += os.sep
88
89
 
89
90
  position = position.replace("\\", "/")
91
+ if progress_callback:
92
+ progress_callback(0)
93
+
90
94
  stack, labels = locate_stack_and_labels(
91
95
  position, prefix=prefix, population=population
92
96
  )
93
97
 
98
+ if progress_callback:
99
+ progress_callback(25)
100
+
94
101
  return view_tracks_in_napari(
95
102
  position,
96
103
  population,
@@ -130,7 +137,13 @@ def view_tracks_in_napari(
130
137
  Updated
131
138
  """
132
139
 
140
+ print(f"DEBUG: view_tracks_in_napari called with pos={position}, pop={population}")
133
141
  df, df_path = get_position_table(position, population=population, return_path=True)
142
+ print(f"DEBUG: get_position_table returned df={df is not None}")
143
+
144
+ if progress_callback:
145
+ progress_callback(50)
146
+
134
147
  if df is None:
135
148
  print("Please compute trajectories first... Abort...")
136
149
  return None
@@ -144,12 +157,18 @@ def view_tracks_in_napari(
144
157
 
145
158
  if (labels is not None) * relabel:
146
159
  print("Replacing the cell mask labels with the track ID...")
160
+
161
+ def wrapped_callback(p):
162
+ if progress_callback:
163
+ return progress_callback(50 + int(p * 0.5))
164
+ return True
165
+
147
166
  labels = relabel_segmentation(
148
167
  labels,
149
168
  df,
150
169
  exclude_nans=True,
151
170
  threads=threads,
152
- progress_callback=progress_callback,
171
+ progress_callback=wrapped_callback,
153
172
  )
154
173
  if labels is None:
155
174
  return None
@@ -186,9 +205,12 @@ def launch_napari_viewer(
186
205
  shared_data,
187
206
  contrast_limits,
188
207
  flush_memory=True,
208
+ block=True,
209
+ progress_callback=None,
189
210
  ):
190
211
 
191
212
  viewer = napari.Viewer()
213
+
192
214
  if stack is not None:
193
215
  viewer.add_image(
194
216
  stack,
@@ -196,6 +218,7 @@ def launch_napari_viewer(
196
218
  colormap=["gray"] * stack.shape[-1],
197
219
  contrast_limits=contrast_limits,
198
220
  )
221
+
199
222
  if labels is not None:
200
223
  labels_layer = viewer.add_labels(
201
224
  labels.astype(int), name="segmentation", opacity=0.4
@@ -274,8 +297,6 @@ def launch_napari_viewer(
274
297
  def export_table_widget():
275
298
  return export_modifications()
276
299
 
277
- from celldetective.gui.base.styles import Styles
278
-
279
300
  export_table_widget.native.setStyleSheet(Styles().button_style_sheet)
280
301
 
281
302
  def label_changed(event):
@@ -426,9 +447,9 @@ def launch_napari_viewer(
426
447
 
427
448
  shared_data["df"] = df
428
449
 
429
- viewer.show(block=True)
450
+ viewer.show(block=block)
430
451
 
431
- if flush_memory:
452
+ if flush_memory and block:
432
453
 
433
454
  # temporary fix for slight napari memory leak
434
455
  for i in range(10000):
@@ -781,8 +802,6 @@ def control_segmentation_napari(
781
802
  def export_widget():
782
803
  return export_annotation()
783
804
 
784
- from celldetective.gui.base.styles import Styles
785
-
786
805
  stack, labels = locate_stack_and_labels(
787
806
  position, prefix=prefix, population=population
788
807
  )
@@ -889,22 +908,13 @@ def correct_annotation(filename):
889
908
  stack,
890
909
  channel_axis=-1,
891
910
  colormap=["gray"] * stack.shape[-1],
892
- constrast_limits=contrast_limits,
911
+ contrast_limits=contrast_limits,
893
912
  )
894
913
  viewer.add_labels(labels, name="segmentation", opacity=0.4)
895
914
  viewer.window.add_dock_widget(save_widget, area="right")
896
- viewer.show(block=True)
915
+ save_widget.native.setStyleSheet(Styles().button_style_sheet)
897
916
 
898
- # temporary fix for slight napari memory leak
899
- for i in range(100):
900
- try:
901
- viewer.layers.pop()
902
- except:
903
- pass
904
- del viewer
905
- del stack
906
- del labels
907
- gc.collect()
917
+ viewer.show(block=False)
908
918
 
909
919
 
910
920
  def _view_on_napari(tracks=None, stack=None, labels=None):
@@ -11,101 +11,127 @@ import time
11
11
  from pathlib import Path
12
12
  import json
13
13
 
14
+
14
15
  class DownloadProcess(Process):
15
16
 
16
- def __init__(self, queue=None, process_args=None, *args, **kwargs):
17
-
18
- super().__init__(*args, **kwargs)
19
-
20
- if process_args is not None:
21
- for key, value in process_args.items():
22
- setattr(self, key, value)
23
-
24
- self.queue = queue
25
- self.progress = True
26
-
27
- file_path = Path(os.path.dirname(os.path.realpath(__file__)))
28
- zenodo_json = os.sep.join([str(file_path.parents[2]),"celldetective", "links", "zenodo.json"])
29
- print(f"{zenodo_json=}")
30
-
31
- with open(zenodo_json,"r") as f:
32
- zenodo_json = json.load(f)
33
- all_files = list(zenodo_json['files']['entries'].keys())
34
- all_files_short = [f.replace(".zip","") for f in all_files]
35
- zenodo_url = zenodo_json['links']['files'].replace('api/','')
36
- full_links = ["/".join([zenodo_url, f]) for f in all_files]
37
- index = all_files_short.index(self.file)
38
-
39
- self.zip_url = full_links[index]
40
- self.path_to_zip_file = os.sep.join([self.output_dir, 'temp.zip'])
41
-
42
- self.sum_done = 0
43
- self.t0 = time.time()
44
-
45
- def download_url_to_file(self, url, dst):
46
- try:
47
- file_size = None
48
- ssl._create_default_https_context = ssl._create_unverified_context
49
- u = urlopen(url)
50
- meta = u.info()
51
- if hasattr(meta, 'getheaders'):
52
- content_length = meta.getheaders("Content-Length")
53
- else:
54
- content_length = meta.get_all("Content-Length")
55
- if content_length is not None and len(content_length) > 0:
56
- file_size = int(content_length[0])
57
- # We deliberately save it in a temp file and move it after
58
- dst = os.path.expanduser(dst)
59
- dst_dir = os.path.dirname(dst)
60
- f = tempfile.NamedTemporaryFile(delete=False, dir=dst_dir)
61
-
62
- try:
63
- with tqdm(total=file_size, disable=not self.progress,
64
- unit='B', unit_scale=True, unit_divisor=1024) as pbar:
65
- while True:
66
- buffer = u.read(8192) #8192
67
- if len(buffer) == 0:
68
- break
69
- f.write(buffer)
70
- pbar.update(len(buffer))
71
- self.sum_done+=len(buffer) / file_size * 100
72
- mean_exec_per_step = (time.time() - self.t0) / (self.sum_done*file_size / 100 + 1)
73
- pred_time = (file_size - (self.sum_done*file_size / 100 + 1)) * mean_exec_per_step
74
- self.queue.put([self.sum_done, pred_time])
75
- f.close()
76
- shutil.move(f.name, dst)
77
- finally:
78
- f.close()
79
- if os.path.exists(f.name):
80
- os.remove(f.name)
81
- except Exception as e:
82
- print("No internet connection: ", e)
83
- return None
84
-
85
- def run(self):
86
-
87
- self.download_url_to_file(fr"{self.zip_url}",self.path_to_zip_file)
88
- with zipfile.ZipFile(self.path_to_zip_file, 'r') as zip_ref:
89
- zip_ref.extractall(self.output_dir)
90
-
91
- file_to_rename = glob(os.sep.join([self.output_dir,self.file,"*[!.json][!.png][!.h5][!.csv][!.npy][!.tif][!.ini]"]))
92
- if len(file_to_rename)>0 and not file_to_rename[0].endswith(os.sep) and not self.file.startswith('demo'):
93
- os.rename(file_to_rename[0], os.sep.join([self.output_dir,self.file,self.file]))
94
-
95
- os.remove(self.path_to_zip_file)
96
- self.queue.put([100,0])
97
- time.sleep(0.5)
98
-
99
- # Send end signal
100
- self.queue.put("finished")
101
- self.queue.close()
102
-
103
- def end_process(self):
104
-
105
- self.terminate()
106
- self.queue.put("finished")
107
-
108
- def abort_process(self):
109
-
110
- self.terminate()
111
- self.queue.put("error")
17
+ def __init__(self, queue=None, process_args=None, *args, **kwargs):
18
+
19
+ super().__init__(*args, **kwargs)
20
+
21
+ if process_args is not None:
22
+ for key, value in process_args.items():
23
+ setattr(self, key, value)
24
+
25
+ self.queue = queue
26
+ self.progress = True
27
+
28
+ # Get celldetective package root
29
+ current_dir = os.path.dirname(os.path.realpath(__file__))
30
+ package_root = os.path.dirname(current_dir)
31
+ zenodo_json = os.path.join(package_root, "links", "zenodo.json")
32
+ # print(f"{zenodo_json=}")
33
+
34
+ with open(zenodo_json, "r") as f:
35
+ zenodo_json = json.load(f)
36
+ all_files = list(zenodo_json["files"]["entries"].keys())
37
+ all_files_short = [f.replace(".zip", "") for f in all_files]
38
+ zenodo_url = zenodo_json["links"]["files"].replace("api/", "")
39
+ full_links = ["/".join([zenodo_url, f]) for f in all_files]
40
+ index = all_files_short.index(self.file)
41
+
42
+ self.zip_url = full_links[index]
43
+ self.path_to_zip_file = os.sep.join([self.output_dir, "temp.zip"])
44
+
45
+ self.sum_done = 0
46
+ self.t0 = time.time()
47
+
48
+ def download_url_to_file(self, url, dst):
49
+ try:
50
+ file_size = None
51
+ ssl._create_default_https_context = ssl._create_unverified_context
52
+ u = urlopen(url)
53
+ meta = u.info()
54
+ if hasattr(meta, "getheaders"):
55
+ content_length = meta.getheaders("Content-Length")
56
+ else:
57
+ content_length = meta.get_all("Content-Length")
58
+ if content_length is not None and len(content_length) > 0:
59
+ file_size = int(content_length[0])
60
+ # We deliberately save it in a temp file and move it after
61
+ dst = os.path.expanduser(dst)
62
+ dst_dir = os.path.dirname(dst)
63
+ f = tempfile.NamedTemporaryFile(delete=False, dir=dst_dir)
64
+
65
+ try:
66
+ with tqdm(
67
+ total=file_size,
68
+ disable=not self.progress,
69
+ unit="B",
70
+ unit_scale=True,
71
+ unit_divisor=1024,
72
+ ) as pbar:
73
+ while True:
74
+ buffer = u.read(8192) # 8192
75
+ if len(buffer) == 0:
76
+ break
77
+ f.write(buffer)
78
+ pbar.update(len(buffer))
79
+ self.sum_done += len(buffer) / file_size * 100
80
+ mean_exec_per_step = (time.time() - self.t0) / (
81
+ self.sum_done * file_size / 100 + 1
82
+ )
83
+ pred_time = (
84
+ file_size - (self.sum_done * file_size / 100 + 1)
85
+ ) * mean_exec_per_step
86
+ self.queue.put([self.sum_done, pred_time])
87
+ f.close()
88
+ shutil.move(f.name, dst)
89
+ finally:
90
+ f.close()
91
+ if os.path.exists(f.name):
92
+ os.remove(f.name)
93
+ except Exception as e:
94
+ print("No internet connection: ", e)
95
+ return None
96
+
97
+ def run(self):
98
+
99
+ self.download_url_to_file(rf"{self.zip_url}", self.path_to_zip_file)
100
+ with zipfile.ZipFile(self.path_to_zip_file, "r") as zip_ref:
101
+ zip_ref.extractall(self.output_dir)
102
+
103
+ file_to_rename = glob(
104
+ os.sep.join(
105
+ [
106
+ self.output_dir,
107
+ self.file,
108
+ "*[!.json][!.png][!.h5][!.csv][!.npy][!.tif][!.ini]",
109
+ ]
110
+ )
111
+ )
112
+ if (
113
+ len(file_to_rename) > 0
114
+ and not file_to_rename[0].endswith(os.sep)
115
+ and not self.file.startswith("demo")
116
+ ):
117
+ os.rename(
118
+ file_to_rename[0], os.sep.join([self.output_dir, self.file, self.file])
119
+ )
120
+
121
+ os.remove(self.path_to_zip_file)
122
+ self.queue.put([100, 0])
123
+ time.sleep(0.5)
124
+
125
+ # Send end signal
126
+ self.queue.put("finished")
127
+ self.queue.close()
128
+
129
+ def end_process(self):
130
+
131
+ self.terminate()
132
+ self.queue.put("finished")
133
+
134
+ def abort_process(self):
135
+
136
+ self.terminate()
137
+ self.queue.put("error")
@@ -0,0 +1,55 @@
1
+ import time
2
+ from multiprocessing import Process
3
+ from celldetective.utils.data_loaders import load_experiment_tables
4
+ from celldetective import get_logger
5
+
6
+ logger = get_logger()
7
+
8
+
9
+ class TableLoaderProcess(Process):
10
+
11
+ def __init__(self, queue=None, process_args=None, *args, **kwargs):
12
+
13
+ super().__init__(*args, **kwargs)
14
+
15
+ if process_args is not None:
16
+ for key, value in process_args.items():
17
+ setattr(self, key, value)
18
+
19
+ self.queue = queue
20
+
21
+ def run(self):
22
+
23
+ def progress(well_progress, pos_progress):
24
+ # Check for cancellation if needed?
25
+ # The runner checks queue for instructions? No, runner closes queue.
26
+ # But here we can just push updates.
27
+ self.queue.put(
28
+ {
29
+ "well_progress": well_progress,
30
+ "pos_progress": pos_progress,
31
+ "status": f"Loading tables... Well {well_progress}%, Position {pos_progress}%",
32
+ }
33
+ )
34
+ return True # continue
35
+
36
+ try:
37
+ self.queue.put({"status": "Started loading..."})
38
+
39
+ df = load_experiment_tables(
40
+ experiment=self.experiment,
41
+ population=self.population,
42
+ well_option=self.well_option,
43
+ position_option=self.position_option,
44
+ return_pos_info=False,
45
+ progress_callback=progress,
46
+ )
47
+
48
+ self.queue.put({"status": "finished", "result": df})
49
+
50
+ except Exception as e:
51
+ logger.error(f"Table loading failed: {e}")
52
+ self.queue.put({"status": "error", "message": str(e)})
53
+
54
+ def end_process(self):
55
+ self.terminate()
@@ -310,6 +310,9 @@ class MeasurementProcess(Process):
310
310
 
311
311
  for t in tqdm(indices, desc="frame"):
312
312
 
313
+ measurements_at_t = None
314
+ perform_measurement = True
315
+
313
316
  if self.file is not None:
314
317
  img = load_frames(
315
318
  self.img_num_channels[:, t],
@@ -321,92 +324,115 @@ class MeasurementProcess(Process):
321
324
  if self.label_path is not None:
322
325
  lbl = locate_labels(self.pos, population=self.mode, frames=t)
323
326
  if lbl is None:
324
- continue
325
-
326
- if self.trajectories is not None:
327
- # Optimized access
328
- if self.frame_slices is not None:
329
- # Check if frame t is in our precomputed slices
330
- if t in self.frame_slices:
331
- start, end = self.frame_slices[t]
332
- positions_at_t = self.trajectories.iloc[start:end].copy()
327
+ perform_measurement = False
328
+
329
+ if perform_measurement:
330
+
331
+ if self.trajectories is not None:
332
+ # Optimized access
333
+ if self.frame_slices is not None:
334
+ # Check if frame t is in our precomputed slices
335
+ if t in self.frame_slices:
336
+ start, end = self.frame_slices[t]
337
+ positions_at_t = self.trajectories.iloc[start:end].copy()
338
+ else:
339
+ # Empty frame for trajectories
340
+ positions_at_t = pd.DataFrame(
341
+ columns=self.trajectories.columns
342
+ )
333
343
  else:
334
- # Empty frame for trajectories
335
- positions_at_t = pd.DataFrame(columns=self.trajectories.columns)
336
- else:
337
- # Fallback or original method (should not be reached if optimized)
338
- positions_at_t = self.trajectories.loc[
339
- self.trajectories[self.column_labels["time"]] == t
340
- ].copy()
341
-
342
- if self.do_features:
343
- feature_table = measure_features(
344
- img,
345
- lbl,
346
- features=self.features,
347
- border_dist=self.border_distances,
348
- channels=self.channel_names,
349
- haralick_options=self.haralick_options,
350
- verbose=False,
351
- normalisation_list=self.background_correction,
352
- spot_detection=self.spot_detection,
353
- )
354
- if self.trajectories is None:
355
- positions_at_t = _extract_coordinates_from_features(
356
- feature_table, timepoint=t
344
+ # Fallback or original method (should not be reached if optimized)
345
+ positions_at_t = self.trajectories.loc[
346
+ self.trajectories[self.column_labels["time"]] == t
347
+ ].copy()
348
+
349
+ if self.do_features:
350
+ feature_table = measure_features(
351
+ img,
352
+ lbl,
353
+ features=self.features,
354
+ border_dist=self.border_distances,
355
+ channels=self.channel_names,
356
+ haralick_options=self.haralick_options,
357
+ verbose=False,
358
+ normalisation_list=self.background_correction,
359
+ spot_detection=self.spot_detection,
360
+ )
361
+ if self.trajectories is None:
362
+ positions_at_t = _extract_coordinates_from_features(
363
+ feature_table, timepoint=t
364
+ )
365
+ column_labels = {
366
+ "track": "ID",
367
+ "time": self.column_labels["time"],
368
+ "x": self.column_labels["x"],
369
+ "y": self.column_labels["y"],
370
+ }
371
+ feature_table.rename(
372
+ columns={
373
+ "centroid-1": "POSITION_X",
374
+ "centroid-0": "POSITION_Y",
375
+ },
376
+ inplace=True,
357
377
  )
358
- column_labels = {
359
- "track": "ID",
360
- "time": self.column_labels["time"],
361
- "x": self.column_labels["x"],
362
- "y": self.column_labels["y"],
363
- }
364
- feature_table.rename(
365
- columns={"centroid-1": "POSITION_X", "centroid-0": "POSITION_Y"},
366
- inplace=True,
367
- )
368
378
 
369
- if self.do_iso_intensities and not self.trajectories is None:
370
- iso_table = measure_isotropic_intensity(
371
- positions_at_t,
372
- img,
373
- channels=self.channel_names,
374
- intensity_measurement_radii=self.intensity_measurement_radii,
375
- column_labels=self.column_labels,
376
- operations=self.isotropic_operations,
377
- verbose=False,
378
- )
379
+ if self.do_iso_intensities and not self.trajectories is None:
380
+ iso_table = measure_isotropic_intensity(
381
+ positions_at_t,
382
+ img,
383
+ channels=self.channel_names,
384
+ intensity_measurement_radii=self.intensity_measurement_radii,
385
+ column_labels=self.column_labels,
386
+ operations=self.isotropic_operations,
387
+ verbose=False,
388
+ )
379
389
 
380
- if (
381
- self.do_iso_intensities
382
- and self.do_features
383
- and not self.trajectories is None
384
- ):
385
- measurements_at_t = iso_table.merge(
386
- feature_table, how="outer", on="class_id", suffixes=("_delme", "")
387
- )
388
- measurements_at_t = measurements_at_t[
389
- [c for c in measurements_at_t.columns if not c.endswith("_delme")]
390
- ]
391
- elif (
392
- self.do_iso_intensities
393
- * (not self.do_features)
394
- * (not self.trajectories is None)
395
- ):
396
- measurements_at_t = iso_table
397
- elif self.do_features:
398
- measurements_at_t = positions_at_t.merge(
399
- feature_table, how="outer", on="class_id", suffixes=("_delme", "")
390
+ if (
391
+ self.do_iso_intensities
392
+ and self.do_features
393
+ and not self.trajectories is None
394
+ ):
395
+ measurements_at_t = iso_table.merge(
396
+ feature_table,
397
+ how="outer",
398
+ on="class_id",
399
+ suffixes=("_delme", ""),
400
+ )
401
+ measurements_at_t = measurements_at_t[
402
+ [
403
+ c
404
+ for c in measurements_at_t.columns
405
+ if not c.endswith("_delme")
406
+ ]
407
+ ]
408
+ elif (
409
+ self.do_iso_intensities
410
+ * (not self.do_features)
411
+ * (not self.trajectories is None)
412
+ ):
413
+ measurements_at_t = iso_table
414
+ elif self.do_features:
415
+ measurements_at_t = positions_at_t.merge(
416
+ feature_table,
417
+ how="outer",
418
+ on="class_id",
419
+ suffixes=("_delme", ""),
420
+ )
421
+ measurements_at_t = measurements_at_t[
422
+ [
423
+ c
424
+ for c in measurements_at_t.columns
425
+ if not c.endswith("_delme")
426
+ ]
427
+ ]
428
+
429
+ measurements_at_t = center_of_mass_to_abs_coordinates(measurements_at_t)
430
+
431
+ measurements_at_t = measure_radial_distance_to_center(
432
+ measurements_at_t,
433
+ volume=img.shape,
434
+ column_labels=self.column_labels,
400
435
  )
401
- measurements_at_t = measurements_at_t[
402
- [c for c in measurements_at_t.columns if not c.endswith("_delme")]
403
- ]
404
-
405
- measurements_at_t = center_of_mass_to_abs_coordinates(measurements_at_t)
406
-
407
- measurements_at_t = measure_radial_distance_to_center(
408
- measurements_at_t, volume=img.shape, column_labels=self.column_labels
409
- )
410
436
 
411
437
  self.sum_done += 1
412
438
  data = {}