microlive 1.0.11__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.
@@ -0,0 +1,1225 @@
1
+ """Pipeline module for MicroLive.
2
+
3
+ This module is part of the microlive package.
4
+ """
5
+ from microlive.imports import *
6
+
7
+ from skimage.feature import canny
8
+ from skimage.draw import circle_perimeter
9
+ from scipy.ndimage import gaussian_filter
10
+ from skimage.filters import threshold_otsu
11
+ from skimage.morphology import binary_opening, binary_closing
12
+ from skimage.measure import label, regionprops
13
+ from skimage.transform import hough_circle, hough_circle_peaks
14
+
15
+ def read_lif_files_in_folder(folder_path):
16
+ # create funtion that read all the .lif files in a folder and return the list of images
17
+ list_folders = list(folder_path.glob('*.lif'))
18
+ return list_folders
19
+
20
+ def find_nearest(array, value):
21
+ # function to find the index of the nearest value in an array
22
+ array = np.asarray(array)
23
+ idx = (np.abs(array - value)).argmin()
24
+ return idx
25
+
26
+ def create_frame_values( image_TZXYC, starting_changing_frame=40, step_size_increase=5):
27
+ N = image_TZXYC.shape[0]
28
+ frame_durations = np.ones(N)
29
+ frame_durations[starting_changing_frame:] = step_size_increase
30
+ frame_values = np.cumsum(frame_durations) - 1 # Subtract 1 to start from 0
31
+ return frame_values
32
+
33
+ def remove_border_masks(array,min_size=50):
34
+ """
35
+ Remove masks that touch the border of the array and reorder the remaining masks.
36
+
37
+ Parameters:
38
+ - array (np.array): The input 2D array with masks.
39
+
40
+ Returns:
41
+ - np.array: The modified array with border-touching masks removed and remaining masks reordered.
42
+ """
43
+ # Get the mask values along the border of the array
44
+
45
+ # Define the minimum size threshold for mask areas
46
+
47
+ # removing small masks
48
+ cleaned_image = array.copy()
49
+ cleaned_image_after_size_threshold = np.zeros_like(array)
50
+ labels_to_keep = np.unique(array)[1:] # Exclude the background
51
+ for label in labels_to_keep:
52
+ mask_area = (cleaned_image == label).sum() # Count the number of pixels in this mask
53
+ if mask_area >= min_size**2:
54
+ cleaned_image_after_size_threshold[cleaned_image == label] = label
55
+
56
+ array = cleaned_image_after_size_threshold
57
+
58
+ top_border = array[0, :]
59
+ bottom_border = array[-1, :]
60
+ left_border = array[:, 0]
61
+ right_border = array[:, -1]
62
+ # Combine all border mask values into a set
63
+ border_masks = set(np.concatenate([top_border, bottom_border, left_border, right_border]))
64
+ # Remove zero (background) from the border mask set
65
+ border_masks.discard(0)
66
+ # Set border touching masks to zero
67
+ for mask_value in border_masks:
68
+ array[array == mask_value] = 0
69
+ # Reorder the remaining masks to have continuous values
70
+ unique_masks = np.unique(array)
71
+ unique_masks = unique_masks[unique_masks != 0]
72
+ # Create a mapping from old mask values to new mask values
73
+ mask_mapping = {old_mask: new_mask for new_mask, old_mask in enumerate(unique_masks, start=1)}
74
+ # Apply the mapping to renumber the remaining masks
75
+ for old_mask, new_mask in mask_mapping.items():
76
+ array[array == old_mask] = new_mask
77
+ return array
78
+
79
+
80
+
81
+ def interpolate_masks(masks, step_size, total_frames):
82
+ interpolated_masks = np.zeros((total_frames, masks.shape[1], masks.shape[2]), dtype=masks.dtype)
83
+ for i in range(0, total_frames, step_size):
84
+ start_mask = masks[i // step_size]
85
+ end_mask = masks[min((i // step_size) + 1, masks.shape[0] - 1)]
86
+ for j in range(step_size):
87
+ if i + j < total_frames:
88
+ t = j / step_size
89
+ interpolated_masks[i + j] = (1 - t) * start_mask + t * end_mask
90
+ return interpolated_masks
91
+
92
+ def find_frap_coordinates(image_TXY, frap_time, stable_FRAP_channel, min_diameter):
93
+ """
94
+ Find the coordinates of the region of interest (ROI) for fluorescence recovery after photobleaching (FRAP).
95
+
96
+ Parameters:
97
+ image_TZXYC_masked : numpy.ndarray
98
+ 5D image array with dimensions corresponding to Time, Z-slice, X, Y, and Channel.
99
+ frap_time : int
100
+ Time point of the FRAP event (1-indexed).
101
+ stable_FRAP_channel : int
102
+ Channel index to be analyzed.
103
+ min_diameter : float
104
+ Minimum diameter for ROI detection in pixels.
105
+
106
+ Returns:
107
+ tuple
108
+ Coordinates of the ROI center and its radius if a suitable region is found, else None.
109
+ """
110
+ z = 0 # Use first Z-slice, adjust if needed
111
+ channel = stable_FRAP_channel # The channel to use for detection
112
+ if frap_time <= 0:
113
+ raise ValueError("frap_time must be greater than 0 to compare frames.")
114
+
115
+ # Extract frames before and after FRAP
116
+ image_before_frap = image_TXY[frap_time - 1] #, :, :, channel]
117
+ image_after_frap = image_TXY[frap_time]#, :, :, channel]
118
+
119
+ # Compute the difference image
120
+ diff_image = image_after_frap.astype(np.float32) - image_before_frap.astype(np.float32)
121
+ neg_diff_image = -diff_image # Regions with decreased intensity are positive
122
+
123
+ # Threshold the negative difference image using Otsu's method
124
+ threshold = threshold_otsu(neg_diff_image)
125
+ binary_diff = neg_diff_image > threshold
126
+
127
+ # Clean up the binary image using morphological operations
128
+ binary_diff = binary_opening_ndi(binary_diff, structure=np.ones((3, 3)))
129
+ binary_diff = binary_closing_ndi(binary_diff, structure=np.ones((3, 3)))
130
+
131
+ # Label regions and extract properties
132
+ labeled_regions = label(binary_diff)
133
+ props = regionprops(labeled_regions)
134
+
135
+ # Expected area based on min_diameter
136
+ expected_area = np.pi * (min_diameter / 2) ** 2
137
+ area_tolerance = expected_area * 0.5 # Adjust as needed
138
+
139
+ # Initialize variables to keep track of the best region
140
+ best_region = None
141
+ best_circularity = 0
142
+ for prop in props:
143
+ area = prop.area
144
+ perimeter = prop.perimeter
145
+ if perimeter == 0:
146
+ continue # Avoid division by zero
147
+ circularity = (4 * np.pi * area) / (perimeter ** 2)
148
+ area_difference = abs(area - expected_area)
149
+ # Check if area is within acceptable range and circularity is high
150
+ if area_difference <= area_tolerance and circularity > best_circularity:
151
+ best_region = prop
152
+ best_circularity = circularity
153
+ break
154
+
155
+ # Return the coordinates and radius if a suitable region was found
156
+ if best_region:
157
+ #print(best_region.centroid[1], best_region.centroid[0] )
158
+ return best_region.centroid[1], best_region.centroid[0]
159
+ else:
160
+ return None, None
161
+
162
+
163
+ def segment_image(image_TXY, step_size=5, pretrained_model_segmentation=None, frap_time=None, pixel_dilation_pseudo_cytosol=10,stable_FRAP_channel=0,min_diameter=10):
164
+ num_pixels_to_dilate = 1
165
+ use_gpu = False # or True if you want to try MPS on Apple Silicon
166
+ if pretrained_model_segmentation is not None:
167
+ model_nucleus = models.CellposeModel(
168
+ gpu=use_gpu,
169
+ pretrained_model=pretrained_model_segmentation
170
+ )
171
+ else:
172
+ model_nucleus = models.CellposeModel(
173
+ gpu=use_gpu,
174
+ model_type='nuclei'
175
+ )
176
+ #model_cyto = models.Cellpose(gpu=False, model_type='cyto2')
177
+ model_cyto = models.CellposeModel( gpu=use_gpu, model_type='cyto2')
178
+ num_steps = (image_TXY.shape[0] + step_size - 1) // step_size
179
+ list_masks = []
180
+ list_selected_mask_id = []
181
+ list_selected_masks = []
182
+ list_masks_cyto = []
183
+ # If frap_time is provided, segment the FRAP images and select the mask with maximum intensity change
184
+ if frap_time is not None:
185
+ # Ensure frap_time is within valid range
186
+ if frap_time < 1 or frap_time >= image_TXY.shape[0] - 1:
187
+ raise ValueError("frap_time must be within the range of the image stack.")
188
+ # Segment the image at frap_time
189
+ #masks_frap = model_nucleus.eval(image_TXY[frap_time], normalize=True, channels=[0,0], flow_threshold=0.8, diameter=150, min_size=100)[0]
190
+ masks_frap = model_nucleus.eval(
191
+ image_TXY[frap_time],
192
+ channels=[0, 0], # ← add this!
193
+ normalize=True,
194
+ flow_threshold=1,
195
+ diameter=150,
196
+ min_size=50
197
+ )[0]
198
+ # remove all the maks that are touching the border
199
+ masks_frap =remove_border_masks(masks_frap,min_size=50)
200
+ # Get unique mask labels (excluding background)
201
+ mask_labels = np.unique(masks_frap)
202
+ mask_labels = mask_labels[mask_labels != 0]
203
+ if mask_labels is not None:
204
+ for label in mask_labels:
205
+ mask = masks_frap == label
206
+ image_TXY_masked = image_TXY * mask
207
+ x_coord_frap_roi, y_coord_frap_roi = find_frap_coordinates(image_TXY_masked, frap_time, stable_FRAP_channel, min_diameter=min_diameter)
208
+ if x_coord_frap_roi is not None:
209
+ selected_mask_frap = mask
210
+ selected_mask_frap = binary_dilation(selected_mask_frap, iterations=num_pixels_to_dilate).astype('int')
211
+ break
212
+ else:
213
+ #selected_mask_id_frap = None
214
+ selected_mask_frap = None
215
+ else:
216
+ #selected_mask_id_frap = None
217
+ selected_mask_frap = None
218
+ else:
219
+ #selected_mask_id_frap = None
220
+ selected_mask_frap = None
221
+ if selected_mask_frap is None:
222
+ return None, None, None
223
+
224
+ for step in range(num_steps):
225
+ i = step * step_size
226
+ # Detecting masks in i-th frame
227
+ masks = model_nucleus.eval(
228
+ image_TXY[i],
229
+ channels=[0, 0],
230
+ normalize=True,
231
+ flow_threshold=1,
232
+ diameter=150,
233
+ min_size=50
234
+ )[0]
235
+ list_masks.append(masks)
236
+ masks =remove_border_masks(masks,min_size=50)
237
+ # Detect cytosol masks only every `step_size` frames
238
+ if step % 2 == 0:
239
+ masks_cyto = model_cyto.eval(image_TXY[i], normalize=True, flow_threshold=0.5, diameter=250, min_size=100)[0]
240
+ list_masks_cyto.append(masks_cyto)
241
+ if frap_time is None:
242
+ # Selecting the mask that is in the center of the image
243
+ selected_mask_id = masks[masks.shape[0] // 2, masks.shape[1] // 2]
244
+ if selected_mask_id == 0 and step > 0:
245
+ selected_mask_id = list_selected_mask_id[step - 1]
246
+ list_selected_mask_id.append(selected_mask_id)
247
+ selected_masks = masks == selected_mask_id
248
+ else:
249
+ # Find the mask in `masks` that overlaps most with `selected_mask_frap`
250
+ labels = np.unique(masks)
251
+ labels = labels[labels != 0]
252
+ max_overlap = 0
253
+ selected_mask_id = None
254
+ for label in labels:
255
+ current_mask = masks == label
256
+ overlap = np.sum(current_mask & selected_mask_frap)
257
+ if overlap > max_overlap:
258
+ max_overlap = overlap
259
+ selected_mask_id = label
260
+ if selected_mask_id is None:
261
+ # Use the previous selected_mask_id or default to the first label
262
+ if step > 0:
263
+ selected_mask_id = list_selected_mask_id[step - 1]
264
+ else:
265
+ selected_mask_id = labels[0] if len(labels) > 0 else 0
266
+ list_selected_mask_id.append(selected_mask_id)
267
+ selected_masks = masks == selected_mask_id
268
+ # Dilate the selected mask
269
+ selected_masks = binary_dilation(selected_masks, iterations=num_pixels_to_dilate).astype('int')
270
+ list_selected_masks.append(selected_masks)
271
+ # Ensure selected nuclear masks persist if segmentation fails in a frame
272
+ for idx, mask in enumerate(list_selected_masks):
273
+ if np.sum(mask) == 0:
274
+ if idx > 0:
275
+ list_selected_masks[idx] = list_selected_masks[idx - 1]
276
+ else:
277
+ # Use first non-empty mask for initial frame if needed
278
+ for m in list_selected_masks:
279
+ if np.sum(m) > 0:
280
+ list_selected_masks[0] = m
281
+ break
282
+ # Interpolating masks for frames between the steps
283
+ masks_TXY = interpolate_masks(np.array(list_selected_masks), step_size, image_TXY.shape[0])
284
+ # Ensure that each time point has a non-empty mask
285
+ non_empty_indices = np.where(np.sum(masks_TXY, axis=(1, 2)) > 0)[0]
286
+ if not non_empty_indices.size:
287
+ raise ValueError("All masks are empty. Check the segmentation parameters.")
288
+ for i in range(masks_TXY.shape[0]):
289
+ if np.sum(masks_TXY[i]) == 0:
290
+ # Find the closest non-empty mask
291
+ closest_idx = non_empty_indices[np.argmin(np.abs(non_empty_indices - i))]
292
+ masks_TXY[i] = masks_TXY[closest_idx]
293
+ all_cyto_masks = interpolate_masks(np.array(list_masks_cyto), step_size * 2, image_TXY.shape[0])
294
+ all_nucleus_masks = np.array(list_masks)
295
+ sum_masks = binary_dilation(np.sum(all_nucleus_masks, axis=0), iterations=20).astype('int')
296
+ sum_cyto_masks = binary_dilation(np.sum(all_cyto_masks, axis=0), iterations=20).astype('int')
297
+ background_mask = (sum_masks + sum_cyto_masks) == 0
298
+
299
+ # Create pseudo-cytosol masks
300
+ pseudo_cytosol_masks_TXY = np.zeros_like(masks_TXY)
301
+ # Dilate the nucleus masks
302
+ dilated_nucleus_masks = binary_dilation(masks_TXY, iterations=5)
303
+ for i in range(masks_TXY.shape[0]):
304
+ dilated_nucleus_masks[i] = binary_dilation(masks_TXY[i], iterations=pixel_dilation_pseudo_cytosol).astype('int')
305
+ # Subtract the dilated nucleus masks from the sum of all masks
306
+ temp_pseudo_cytosol_masks_TXY = dilated_nucleus_masks[i] - masks_TXY[i]
307
+ pseudo_cytosol_masks_TXY[i] = temp_pseudo_cytosol_masks_TXY > 0
308
+ pseudo_cytosol_masks_TXY = pseudo_cytosol_masks_TXY.astype('int')
309
+ return masks_TXY, background_mask, pseudo_cytosol_masks_TXY
310
+
311
+
312
+
313
+
314
+ def create_image_arrays(list_concatenated_images, selected_image=0, FRAP_channel_to_quantify=0,pretrained_model_segmentation=None,frap_time=None, starting_changing_frame=40, step_size_increase=5,min_diameter=10):
315
+ image_TZXYC = list_concatenated_images[selected_image] # shape (T Z Y X C)
316
+ print('Image with shape (T Z Y X C):\n ' ,list_concatenated_images[selected_image].shape) # TZYXC
317
+ print('Original Image pixel ', 'min: {:.2f}, max: {:.2f}, mean: {:.2f}, std: {:.2f}'.format(np.min(image_TZXYC), np.max(image_TZXYC), np.mean(image_TZXYC), np.std(image_TZXYC)) )
318
+
319
+
320
+ image_TXY = image_TZXYC[:,0,:,:,FRAP_channel_to_quantify] # shape (T X Y)
321
+ image_TXY_8bit = (image_TXY - np.min(image_TXY)) / (np.max(image_TXY) - np.min(image_TXY)) * 255
322
+
323
+ # create image_TXY_8bit_stable_FRAP_channel
324
+ if FRAP_channel_to_quantify == 0:
325
+ stable_FRAP_channel = 1
326
+ else:
327
+ stable_FRAP_channel = 0
328
+ image_TXY_stable_FRAP = image_TZXYC[:,0,:,:,stable_FRAP_channel] # shape (T X Y)
329
+ image_TXY_stable_FRAP_8bit = (image_TXY_stable_FRAP - np.min(image_TXY_stable_FRAP)) / (np.max(image_TXY_stable_FRAP) - np.min(image_TXY_stable_FRAP)) * 255
330
+
331
+ masks_TXY, background_mask, pseudo_cytosol_masks_TXY = segment_image(image_TXY_stable_FRAP_8bit, step_size=5, pretrained_model_segmentation=pretrained_model_segmentation,frap_time=frap_time,stable_FRAP_channel=FRAP_channel_to_quantify,min_diameter=min_diameter)
332
+
333
+ if masks_TXY is None:
334
+ return None, None, None, None, None, None, None
335
+ masks_TZXYC = masks_TXY[..., np.newaxis, np.newaxis] # Now shape is (T, Y, X, 1, 1)
336
+ masks_TZXYC = np.transpose(masks_TZXYC, (0, 3, 1, 2, 4)).astype('bool') # Change to (T, 1, Y, X, 1)
337
+ image_TZXYC_masked = image_TZXYC * masks_TZXYC
338
+ frame_values = create_frame_values(image_TZXYC, starting_changing_frame=starting_changing_frame, step_size_increase=step_size_increase)
339
+ return image_TZXYC, image_TZXYC_masked, image_TXY, masks_TXY, pseudo_cytosol_masks_TXY, background_mask, frame_values
340
+
341
+
342
+ def concatenate_images(list_images, list_names, convert_to_8bit=False, list_time_intervals=None):
343
+ """
344
+ Concatenates sets of images based on scene and series order, ensuring consistency across scenes and series order.
345
+
346
+ Args:
347
+ list_images (list of numpy.ndarray): List of images to be concatenated.
348
+ list_names (list of str): Corresponding names of the images indicating scene/series.
349
+
350
+ Returns:
351
+ tuple: Two lists containing the concatenated images and their corresponding names.
352
+ """
353
+ # Reading the file names and extracting the scene and series
354
+ list_scenes = [name.split('/')[0] for name in list_names]
355
+ list_unique_scenes = list(set(list_scenes))
356
+ # sort list_unique_scenes
357
+ list_unique_scenes.sort()
358
+ list_concatenated_images = []
359
+ list_names_concatenated_images = []
360
+ list_time_concatenated = []
361
+ for i, cell_id in enumerate(list_unique_scenes):
362
+ # find the index of list_names that contain the cell_id and string 'Pre'
363
+ try:
364
+ index_pre = [i for i, name in enumerate(list_names) if cell_id in name and 'Pre' in name][0]
365
+ except Exception:
366
+ continue
367
+ # find the index of list_names that contain the cell_id and string 'Pb1'
368
+ try:
369
+ index_pb1 = [i for i, name in enumerate(list_names) if cell_id in name and 'Pb1' in name][0]
370
+ except Exception:
371
+ continue
372
+
373
+ # attempt to find second post-bleach ("Pb2"), but allow if missing
374
+ try:
375
+ index_pb2 = [i for i, name in enumerate(list_names) if cell_id in name and 'Pb2' in name][0]
376
+ except IndexError:
377
+ index_pb2 = None
378
+
379
+ # build list of images to concatenate: always Pre and Pb1, add Pb2 if it exists
380
+ images_to_concat = [list_images[index_pre], list_images[index_pb1]]
381
+ if index_pb2 is not None:
382
+ images_to_concat.append(list_images[index_pb2])
383
+
384
+ # build list of time intervals similarly
385
+ if list_time_intervals is not None:
386
+ times_to_concat = [list_time_intervals[index_pre], list_time_intervals[index_pb1]]
387
+ if index_pb2 is not None:
388
+ times_to_concat.append(list_time_intervals[index_pb2])
389
+ else:
390
+ times_to_concat = None
391
+
392
+ # concatenate along the time axis
393
+ image = np.concatenate(images_to_concat, axis=0)
394
+ list_time = times_to_concat
395
+
396
+ if convert_to_8bit:
397
+ # number of channels
398
+ number_channels = image.shape[-1]
399
+ for ch in range(number_channels):
400
+ image[..., ch] = (image[..., ch] - np.min(image[..., ch])) / (np.max(image[..., ch]) - np.min(image[..., ch])) * 255
401
+ image[..., ch] = image[..., ch].astype(np.uint8)
402
+ list_concatenated_images.append(image)
403
+ list_names_concatenated_images.append(cell_id)
404
+ list_time_concatenated.append(list_time)
405
+ return list_concatenated_images, list_names_concatenated_images, list_time_concatenated
406
+
407
+
408
+
409
+ def calculate_mask_and_background_intensity(image_TXY, masks_TXY,background_mask,pseudo_cytosol_masks_TXY):
410
+ number_frames = image_TXY.shape[0]
411
+ mask_intensity_nucleus = np.zeros((number_frames))
412
+ mask_intensity_background = np.zeros((number_frames))
413
+ mask_intensity_pseudo_cytosol = np.zeros((number_frames))
414
+ for i in range(number_frames):
415
+ mask = masks_TXY[i] == 1
416
+ pseudo_cytosol_mask = pseudo_cytosol_masks_TXY[i] == 1
417
+ mask_intensity_nucleus[i] = np.mean(image_TXY[i][mask>0])
418
+ mask_intensity_background[i] = np.mean(image_TXY[i][background_mask>0])
419
+ mask_intensity_pseudo_cytosol[i] = np.mean(image_TXY[i][pseudo_cytosol_mask>0])
420
+ return mask_intensity_nucleus, mask_intensity_background, mask_intensity_pseudo_cytosol
421
+
422
+ def get_roi_pixel_values(
423
+ image_TZXYC,
424
+ coordinates_roi,
425
+ radius_roi_size_px,
426
+ selected_color_channel=0,
427
+ mask_intensity_background=None):
428
+
429
+ list_roi_pixel_values = []
430
+ for j in range(coordinates_roi.shape[0]):
431
+ x = coordinates_roi[j, 0]
432
+ y = coordinates_roi[j, 1]
433
+ # Define bounding box around the ROI
434
+ x_min = int(np.floor(x - radius_roi_size_px))
435
+ x_max = int(np.ceil(x + radius_roi_size_px))
436
+ y_min = int(np.floor(y - radius_roi_size_px))
437
+ y_max = int(np.ceil(y + radius_roi_size_px))
438
+ # Ensure the indices are within image bounds
439
+ x_min = max(x_min, 0)
440
+ y_min = max(y_min, 0)
441
+ x_max = min(x_max, image_TZXYC.shape[3] - 1)
442
+ y_max = min(y_max, image_TZXYC.shape[2] - 1)
443
+ # Extract the sub-image for the current time point and channel
444
+ sub_image = image_TZXYC[j, 0, y_min:y_max+1, x_min:x_max+1, selected_color_channel]
445
+ # Create a grid of coordinates within the bounding box
446
+ yy_indices = np.arange(y_min, y_max+1)
447
+ xx_indices = np.arange(x_min, x_max+1)
448
+ yy, xx = np.meshgrid(yy_indices, xx_indices, indexing='ij')
449
+ # Compute the distance of each pixel from the ROI center
450
+ distance = np.sqrt((xx - x) ** 2 + (yy - y) ** 2)
451
+ # Create a circular mask
452
+ mask = distance <= radius_roi_size_px
453
+ # Apply the mask to the sub-image to get pixels within the circle
454
+ roi_pixels = sub_image[mask]
455
+ # Compute the mean intensity of the pixels within the circle
456
+ mean_intensity = np.mean(roi_pixels)
457
+ list_roi_pixel_values.append(mean_intensity)
458
+ mean_roi_frap = np.array(list_roi_pixel_values)
459
+ if mask_intensity_background is not None:
460
+ mean_roi_frap = mean_roi_frap - mask_intensity_background
461
+ return mean_roi_frap
462
+
463
+
464
+ def clean_binary_image(binary_image):
465
+ selem = disk(1)
466
+ cleaned_image = binary_closing(binary_opening(binary_image, selem), selem)
467
+ return cleaned_image.astype(bool)
468
+
469
+ # Define the image_binarization function
470
+ def image_binarization(image_TXYC, stable_FRAP_channel=0, invert_image=True, max_percentile=80, binary_iterations=2):
471
+ image_TXY = image_TXYC[:, :, :, stable_FRAP_channel]
472
+ image_TXY = gaussian_filter(image_TXY, sigma=1)
473
+ image_TXY_binarized = np.zeros_like(image_TXY, dtype=np.uint8)
474
+ for i in range(image_TXY.shape[0]):
475
+ # Adaptive thresholding
476
+ threshold = np.percentile(image_TXY[i], max_percentile)
477
+ binary_mask = image_TXY[i] > threshold
478
+ # Clean the binary image using binary morphological operations
479
+ binary_mask = clean_binary_image(binary_mask)
480
+ # Dilate the binary mask using scipy.ndimage's binary_dilation
481
+ if invert_image:
482
+ dilated_mask = binary_dilation(binary_mask, iterations=binary_iterations).astype('int')
483
+ image_TXY_binarized[i] = 1 - dilated_mask
484
+ else:
485
+ image_TXY_binarized[i] = binary_dilation(binary_mask, iterations=binary_iterations).astype('int')
486
+ return image_TXY_binarized
487
+
488
+ def find_roi_centroids(image_TXY,masks_TXY, min_diameter=10):
489
+ """
490
+ Segment images and find centroids of objects larger than a given minimum diameter.
491
+
492
+ Args:
493
+ image_TXY (numpy.ndarray): A 3D numpy array where each slice is a frame.
494
+ min_diameter (float): Minimum diameter of objects to consider.
495
+
496
+ Returns:
497
+ pd.DataFrame: DataFrame with columns ['frame', 'x', 'y'] containing centroids of filtered objects.
498
+ """
499
+ all_centroids = []
500
+ min_area = np.pi * (min_diameter / 2) ** 2 # Convert diameter to minimum area for a circular approximation
501
+ for i, image in enumerate(image_TXY):
502
+ # Label the objects in the image
503
+ labeled_array, _ = ndi_label(image) # label
504
+ props = regionprops(labeled_array)
505
+ # Filter properties based on area and append centroids
506
+ for prop in props:
507
+ if prop.area >= min_area: # Check if the area of the object is greater than the minimum area
508
+ # create a temporal df containing only the x and y coordinates
509
+ df_located_elements = pd.DataFrame(prop.coords, columns=['y', 'x'])
510
+ df_elements_in_mask = mi.Utilities().spots_in_mask(df_located_elements, masks_TXY[i])
511
+ if df_elements_in_mask['In Mask'].sum() > 0:
512
+ centroid = prop.centroid # Centroid as (row, col)
513
+ all_centroids.append({
514
+ 'frame': i,
515
+ 'y': centroid[0], # Row is Y coordinate
516
+ 'x': centroid[1], # Column is X coordinate
517
+ 'solidity': prop.solidity,
518
+ 'equivalent_diameter' : prop.equivalent_diameter,
519
+ 'filled_area': prop.filled_area,
520
+ 'perimeter': prop.perimeter,
521
+ 'extent': prop.extent,
522
+ 'area':prop.area,
523
+ # calculate the intensity of a crop around the centroid
524
+ 'mean_intensity': np.mean(image[int(centroid[0])-10:int(centroid[0])+10,int(centroid[1])-10:int(centroid[1])+10])
525
+ })
526
+ # Create DataFrame
527
+ centroids_df = pd.DataFrame(all_centroids)
528
+ return centroids_df
529
+
530
+
531
+ def detect_roi_by_difference(
532
+ image_TZXYC_masked, # shape (T, Z, Y, X, C)
533
+ image_TZXYC, # shape (T, Z, Y, X, C)
534
+ masks_TXY, # shape (T, Y, X)
535
+ frap_time, # int
536
+ min_diameter, # float
537
+ stable_FRAP_channel, # int
538
+ max_roi_displacement_px=None
539
+ ):
540
+ """
541
+ Detect the FRAP ROI by comparing pre- and post-bleach frames, then
542
+ validate each candidate by its intensity trace in the masked image.
543
+ Returns:
544
+ coordinates_roi: np.ndarray of shape (T,2) giving (x,y) per frame
545
+ df_selected_trajectory: pd.DataFrame with columns ['frame','x','y']
546
+ or (None, None) if detection fails.
547
+ """
548
+
549
+ T = image_TZXYC.shape[0]
550
+ z = 0
551
+ ch = stable_FRAP_channel
552
+
553
+ # sanity check
554
+ if frap_time <= 0 or frap_time >= T:
555
+ return None, None
556
+
557
+ # compute difference image
558
+ before = image_TZXYC_masked[frap_time-1, z, :, :, ch].astype(np.float32)
559
+ after = image_TZXYC_masked[frap_time , z, :, :, ch].astype(np.float32)
560
+ diff = gaussian_filter(after - before, sigma=1)
561
+ neg = -diff
562
+
563
+ # threshold + clean
564
+ try:
565
+ thr = threshold_otsu(neg)
566
+ except ValueError:
567
+ return None, None
568
+ bw = neg > thr
569
+ bw = binary_opening(bw, footprint=np.ones((3,3)))
570
+ bw = binary_closing(bw, footprint=np.ones((3,3)))
571
+
572
+ # find candidate regions
573
+ lbl = label(bw)
574
+ props = regionprops(lbl)
575
+ exp_area = np.pi * (min_diameter/2)**2
576
+ tol = exp_area * 0.5
577
+
578
+ candidates = []
579
+ for p in props:
580
+ if p.perimeter == 0:
581
+ continue
582
+ area = p.area
583
+ circ = (4*np.pi*area) / (p.perimeter**2)
584
+ if abs(area - exp_area) <= tol:
585
+ candidates.append((circ, p.centroid)) # centroid = (y,x)
586
+
587
+ # Hough fallback if no regionprops
588
+ if not candidates:
589
+ edges = canny(neg, sigma=2)
590
+ r = int(round(min_diameter/2))
591
+ hres = hough_circle(edges, [r])
592
+ acc, cx, cy, rad = hough_circle_peaks(hres, [r], total_num_peaks=1)
593
+ if cx.size:
594
+ candidates = [(1.0, (cy[0], cx[0]))]
595
+
596
+ # sort by circularity
597
+ candidates.sort(key=lambda x: x[0], reverse=True)
598
+
599
+ radius = min_diameter/2
600
+
601
+ # test each candidate by its ROI intensity trace
602
+ for circ, (cy, cx) in candidates:
603
+ # build track
604
+ coords = np.tile([cx, cy], (T,1))
605
+
606
+ # clamp displacements
607
+ if max_roi_displacement_px is not None:
608
+ deltas = np.diff(coords, axis=0)
609
+ too_big = np.sum(deltas**2, axis=1) > max_roi_displacement_px**2
610
+ for idx in np.where(too_big)[0] + 1:
611
+ coords[idx] = coords[idx-1]
612
+
613
+ # sample intensity trace
614
+ roi_int = get_roi_pixel_values(
615
+ image_TZXYC=image_TZXYC_masked,
616
+ coordinates_roi=coords,
617
+ radius_roi_size_px=radius,
618
+ selected_color_channel=ch,
619
+ mask_intensity_background=None
620
+ )
621
+
622
+ # validate: stable before bleach
623
+ base = roi_int[:frap_time]
624
+ baseline, pre_std = base.mean(), base.std()
625
+ drop = roi_int[frap_time+1]
626
+ recovery = roi_int[-1] - roi_int[frap_time]
627
+
628
+ if drop <= baseline*0.6: # and recovery >= drop*0.1:
629
+ # success
630
+ df = pd.DataFrame({
631
+ 'frame': np.arange(T),
632
+ 'x': coords[:,0],
633
+ 'y': coords[:,1]
634
+ })
635
+ return coords, df
636
+
637
+ # no candidate passed
638
+ return None, None
639
+
640
+
641
+ def detect_roi_by_tracking(
642
+ image_TZXYC_masked,
643
+ image_TZXYC,
644
+ masks_TXY,
645
+ frap_time,
646
+ min_diameter,
647
+ stable_FRAP_channel,
648
+ use_frap_time_for_roi_detection,
649
+ max_roi_displacement_px,
650
+ FRAP_channel_to_quantify=0,
651
+ show_binary_plot=False,
652
+ list_selected_frames=[0, 10, 40, 100, 139],
653
+ list_selected_frame_values_real_time=None,
654
+ mask_intensity_background=None,
655
+ ):
656
+ """
657
+ Fallback: TrackPy-based detection, wrap in try/except.
658
+ """
659
+ try:
660
+ # Use similar logic as in find_frap_roi's else branch
661
+ list_max_percentile = np.linspace(95, 100, 5, dtype=int)
662
+ list_binary_iterations = np.linspace(1, 10, 5, dtype=int)
663
+ num_frames = image_TZXYC.shape[0]
664
+ break_condition_met = False
665
+ for max_percentile in list_max_percentile:
666
+ for binary_iterations in list_binary_iterations:
667
+ try:
668
+ image_TXY_binarized = image_binarization(
669
+ image_TZXYC_masked[:, 0, :, :, :],
670
+ stable_FRAP_channel=stable_FRAP_channel,
671
+ invert_image=True,
672
+ max_percentile=max_percentile,
673
+ binary_iterations=binary_iterations
674
+ )
675
+ min_frames_for_linking = image_TXY_binarized.shape[0] // 2
676
+ minimal_distance_between_centroids = 4
677
+ centroids_df_before = find_roi_centroids(image_TXY_binarized[:1], masks_TXY[:1], min_diameter )
678
+ centroids_df_after = find_roi_centroids(image_TXY_binarized[frap_time:], masks_TXY[frap_time:], min_diameter )
679
+ def min_distance_to_before(row, centroids_df_before):
680
+ distances = np.sqrt( (centroids_df_before['x'] - row['x']) ** 2 + (centroids_df_before['y'] - row['y']) ** 2)
681
+ return distances.min()
682
+ centroids_df_after['min_distance'] = centroids_df_after.apply(min_distance_to_before,axis=1, args=(centroids_df_before,))
683
+ centroids_df = centroids_df_after[
684
+ centroids_df_after['min_distance'] >= minimal_distance_between_centroids
685
+ ]
686
+ dataframe_linked_elements = tp.link(centroids_df, search_range=7, memory=1)
687
+ df_roi = tp.filter_stubs(dataframe_linked_elements, min_frames_for_linking)
688
+ if df_roi.empty:
689
+ continue
690
+ else:
691
+ list_particles = df_roi['particle'].unique()
692
+ for particle_id in list_particles:
693
+ df_selected_trajectory = df_roi[df_roi['particle'] == particle_id]
694
+ df_selected_trajectory.reset_index(drop=True, inplace=True)
695
+ number_frames_after_frap = image_TXY_binarized.shape[0] - frap_time
696
+ all_frames = pd.DataFrame({'frame': range(number_frames_after_frap)})
697
+ df_full = pd.merge(all_frames,df_selected_trajectory,on='frame', how='left')
698
+ df_full.fillna(method='ffill', inplace=True)
699
+ df_full.fillna(method='bfill', inplace=True)
700
+ df_selected_trajectory = df_full
701
+ num_new_rows = frap_time
702
+ new_rows = pd.DataFrame([df_selected_trajectory.iloc[0]] * num_new_rows)
703
+ new_rows['frame'] = range(0, num_new_rows)
704
+ df_selected_trajectory = pd.concat([new_rows, df_selected_trajectory], ignore_index=True)
705
+ df_selected_trajectory['frame'] = range(df_selected_trajectory.shape[0])
706
+ df_selected_trajectory.sort_values('frame', inplace=True)
707
+ df_selected_trajectory.reset_index(drop=True, inplace=True)
708
+ coordinates_roi = df_selected_trajectory[['x', 'y']].values
709
+ if max_roi_displacement_px is not None:
710
+ deltas = np.diff(coordinates_roi, axis=0)
711
+ d2 = np.sum(deltas**2, axis=1)
712
+ mask = d2 > (max_roi_displacement_px ** 2)
713
+ if np.any(mask):
714
+ idxs = np.where(mask)[0] + 1
715
+ for idx in idxs:
716
+ coordinates_roi[idx] = coordinates_roi[idx-1]
717
+ # Ensure track stays within the selected nucleus mask
718
+ mask_hits = np.array([
719
+ masks_TXY[i, int(round(coord[1])), int(round(coord[0]))]
720
+ for i, coord in enumerate(coordinates_roi)
721
+ ])
722
+ # if less than 80% of points lie within the mask, skip this track
723
+ if np.mean(mask_hits) < 0.8:
724
+ continue
725
+ return coordinates_roi, df_selected_trajectory
726
+ except Exception:
727
+ continue
728
+ return None, None
729
+ except Exception:
730
+ return None, None
731
+
732
+
733
+ def find_frap_roi(
734
+ image_TZXYC_masked,
735
+ image_TZXYC,
736
+ masks_TXY,
737
+ frap_time,
738
+ min_diameter=10,
739
+ FRAP_channel_to_quantify=0,
740
+ stable_FRAP_channel=0,
741
+ show_binary_plot=False,
742
+ list_selected_frames=[0, 10, 40, 100, 139],
743
+ list_selected_frame_values_real_time=None,
744
+ mask_intensity_background=None,
745
+ use_frap_time_for_roi_detection=True,
746
+ max_roi_displacement_px=5
747
+ ):
748
+ """
749
+ New function to find FRAP ROI using difference or tracking.
750
+ Returns (mean_roi_frap, mean_roi_frap_normalized, coordinates_roi, df_selected_trajectory)
751
+ """
752
+ coordinates_roi = None
753
+ df_selected_trajectory = None
754
+ if use_frap_time_for_roi_detection:
755
+ coordinates_roi, df_selected_trajectory = detect_roi_by_difference(
756
+ image_TZXYC_masked,
757
+ image_TZXYC,
758
+ masks_TXY,
759
+ frap_time,
760
+ min_diameter,
761
+ stable_FRAP_channel,
762
+ max_roi_displacement_px,
763
+ )
764
+ else:
765
+ coordinates_roi, df_selected_trajectory = detect_roi_by_tracking(
766
+ image_TZXYC_masked,
767
+ image_TZXYC,
768
+ masks_TXY,
769
+ frap_time,
770
+ min_diameter,
771
+ stable_FRAP_channel,
772
+ use_frap_time_for_roi_detection,
773
+ max_roi_displacement_px,
774
+ FRAP_channel_to_quantify=FRAP_channel_to_quantify,
775
+ show_binary_plot=show_binary_plot,
776
+ list_selected_frames=list_selected_frames,
777
+ list_selected_frame_values_real_time=list_selected_frame_values_real_time,
778
+ mask_intensity_background=mask_intensity_background,
779
+ )
780
+ if coordinates_roi is not None and df_selected_trajectory is not None:
781
+ radius_roi_size_px = min_diameter / 2
782
+ mean_roi_frap = get_roi_pixel_values(
783
+ image_TZXYC=image_TZXYC,
784
+ coordinates_roi=coordinates_roi,
785
+ radius_roi_size_px=radius_roi_size_px,
786
+ selected_color_channel=FRAP_channel_to_quantify,
787
+ mask_intensity_background=mask_intensity_background,
788
+ )
789
+ # Reject if the ROI’s average intensity is too low
790
+ # if np.mean(mean_roi_frap) < 10:
791
+ # return None, None, None, None
792
+ mean_roi_frap_normalized = mean_roi_frap / np.mean(mean_roi_frap[:frap_time])
793
+ return mean_roi_frap, mean_roi_frap_normalized, coordinates_roi, df_selected_trajectory
794
+ else:
795
+ return None, None, None, None
796
+
797
+
798
+
799
+
800
+
801
+
802
+
803
+
804
+ def process_selected_df(df_roi, frap_time, image_TZXYC, FRAP_channel_to_quantify, min_diameter):
805
+ df_selected_trajectory = df_roi[df_roi['particle'] == df_roi['particle'].iloc[0]] # Assuming selection of first found particle
806
+ coordinates_roi = df_selected_trajectory[['x', 'y']].values
807
+ mean_roi_frap = get_roi_pixel_values(image_TZXYC, coordinates_roi, min_diameter, FRAP_channel_to_quantify)
808
+ mean_roi_frap_normalized = mean_roi_frap / np.mean(mean_roi_frap[:frap_time])
809
+ return mean_roi_frap, mean_roi_frap_normalized, coordinates_roi, df_selected_trajectory
810
+
811
+
812
+ def remove_cell_without_roi_detection(df_tracking_all, threhsold=0.08):
813
+ list_selected_df = []
814
+ # removing elements where the code is unable to detect the roi
815
+ for name in df_tracking_all['image_name'].unique():
816
+ df_selected = df_tracking_all[df_tracking_all['image_name'] == name]
817
+ # this code removes the elements where the normalized intensity is lower than
818
+ dif_norm_int = np.diff (df_selected['mean_roi_frap_normalized'])
819
+ # smooth the curve
820
+ dif_norm_int_smoothed = np.convolve(dif_norm_int, np.ones(3)/3, mode='same')
821
+ peaks, _ = find_peaks(-dif_norm_int_smoothed, height=threhsold, distance=7)
822
+ peaks_positive, _ = find_peaks(dif_norm_int_smoothed, height=threhsold, distance=7)
823
+ # if the only one peak is detected, save the dataframe to list_selected_df and the peaks is located in position less than 14 frames
824
+ if len(peaks) == 1 and peaks[0] < 14 and len(peaks_positive) == 0:
825
+ print('Image:', name, 'Peak detected at frame:', peaks[0])
826
+ list_selected_df.append(df_selected)
827
+ else:
828
+ print('Image:', name, 'No peak detected')
829
+ # remove the elements where the code is unable to detect the roi
830
+ df_tracking_removed_roi_no_detected = pd.concat(list_selected_df, ignore_index=True)
831
+ return df_tracking_removed_roi_no_detected
832
+
833
+
834
+
835
+ ##################################################################################
836
+ ##################### Plotting functions #########################################
837
+ ##################################################################################
838
+
839
+
840
+
841
+ def plot_images_frap(image_TZXYC, list_selected_frames=[0, 10, 40, 100, 139], subtitle= None, show_grid=False,
842
+ cmap='viridis', selected_color_channel=None, coordinates_roi=None, radius_roi_size_px=10,
843
+ save_plot=False, plot_name='temp.png',list_selected_frame_values_real_time=None, masks_TXY=None, pseudo_cytosol_masks_TXY=None):
844
+ if selected_color_channel is not None:
845
+ # Reducing dimensions to only selected color channel if specified
846
+ image_TZXYC = image_TZXYC[..., selected_color_channel]
847
+ # Add a dimension at the end to maintain a consistent 5D array format
848
+ image_TZXYC = np.expand_dims(image_TZXYC, axis=-1)
849
+ number_color_channels = image_TZXYC.shape[-1]
850
+ # Check if we need to adjust the subplot setup for only one color channel
851
+ if number_color_channels == 1:
852
+ fig, ax = plt.subplots(1, len(list_selected_frames), figsize=(10, 5))
853
+ ax = np.array([ax]).reshape(1, -1) # Ensure ax is always a 2D array for consistency
854
+ else:
855
+ fig, ax = plt.subplots(number_color_channels, len(list_selected_frames), figsize=(14, 5))
856
+ for ch in range(number_color_channels):
857
+ for i, frame in enumerate(list_selected_frames):
858
+ current_ax = ax[ch, i] if number_color_channels > 1 else ax[0, i]
859
+ current_ax.imshow(image_TZXYC[frame, 0, :, :, ch], vmax=np.percentile(image_TZXYC[frame, 0, :, :, ch], 99.8), cmap=cmap)
860
+ #if masks_TXY is not None:
861
+ # current_ax.contour(masks_TXY[i], colors='r', linewidths=1 , linestyles='solid')
862
+ if pseudo_cytosol_masks_TXY is not None:
863
+ current_ax.contour(pseudo_cytosol_masks_TXY[i], colors='w', linewidths=0.5, linestyles='solid')
864
+
865
+ if list_selected_frame_values_real_time is not None:
866
+ current_ax.set_title(f'{int(list_selected_frame_values_real_time[i])} s', fontsize=10)
867
+ # plot the frame numbers as integers
868
+ else:
869
+ current_ax.set_title(f'Frame index ({int(frame)})', fontsize=10)
870
+ #current_ax.set_title(f'Frame ({frame})', fontsize=10)
871
+ if i == 0:
872
+ current_ax.set_ylabel(f'Ch {ch}' if selected_color_channel is None else f'Selected Ch {selected_color_channel}', fontsize=10)
873
+ if show_grid:
874
+ current_ax.axvline(x=image_TZXYC.shape[3] // 2, color='w', linestyle='-', linewidth=0.5)
875
+ current_ax.axhline(y=image_TZXYC.shape[2] // 2, color='w', linestyle='-', linewidth=0.5)
876
+ else:
877
+ current_ax.grid(False)
878
+ current_ax.set_xticks([])
879
+ current_ax.set_yticks([])
880
+ # plot the roi as a circle
881
+ if coordinates_roi is not None:
882
+ x = coordinates_roi[frame,0]
883
+ y = coordinates_roi[frame,1]
884
+ circle = plt.Circle((x, y), radius_roi_size_px+1, color='orangered', fill=False, linewidth=0.7)
885
+ current_ax.add_artist(circle)
886
+ if subtitle is not None:
887
+ fig.suptitle(subtitle, fontsize=16)
888
+ plt.subplots_adjust(wspace=0.1, hspace=0.1)
889
+ plt.tight_layout()
890
+ if save_plot:
891
+ plt.savefig(plot_name, dpi=300)
892
+ plt.show()
893
+
894
+
895
+ def plot_frap_quantification(frame_values, mean_roi_frap_normalized,mean_roi_frap, mask_intensity_nucleus, mask_intensity_pseudo_cytosol, mask_intensity_background, frap_time,save_plot=False, plot_name='temp.png'):
896
+ use_normalized_frap_int = False
897
+ # plot the intensity of the mask for all frames.
898
+ fig, ax = plt.subplots(1, 4, figsize=(10, 2.5))
899
+ sliding_window = 5
900
+ if use_normalized_frap_int:
901
+ frap_int = mean_roi_frap_normalized
902
+ else:
903
+ frap_int = mean_roi_frap
904
+
905
+ ax[0].plot(frame_values, frap_int, '-r')
906
+ frap_smoothed = np.convolve(frap_int[frap_time+1:], np.ones(sliding_window)/sliding_window, mode='same')
907
+ ax[0].plot(frame_values[frap_time+1:-sliding_window], frap_smoothed[:-sliding_window], '-g', lw=2)
908
+ ax[0].set_xlabel('Frames')
909
+ ax[0].set_ylabel('Mean Pixel Value')
910
+ ax[0].set_title('Intensity', fontsize=10)
911
+
912
+ ax[1].plot(frame_values,mask_intensity_nucleus)
913
+ ax[1].set_title(f'Nucleus', fontsize=10)
914
+
915
+ ax[2].plot(frame_values,mask_intensity_pseudo_cytosol)
916
+ ax[2].set_title(f'pseudo_Cytosol', fontsize=10)
917
+
918
+
919
+ ax[3].plot(frame_values,mask_intensity_background)
920
+ ax[3].set_title('Background', fontsize=10)
921
+
922
+ ax[1].set_xlabel('Frame')
923
+ ax[2].set_xlabel('Frame')
924
+ ax[3].set_xlabel('Frame')
925
+
926
+ ax[1].set_ylabel('Intensity')
927
+ ax[2].set_ylabel('Intensity')
928
+ ax[3].set_ylabel('Intensity')
929
+
930
+ # ensure that the limits are the same for both plots
931
+ ax[1].set_ylim([0, 1.01*np.max(mask_intensity_nucleus)])
932
+ ax[2].set_ylim([0, 1.01*np.max(mask_intensity_nucleus)])
933
+ ax[3].set_ylim([0, 1.01*np.max(mask_intensity_nucleus)])
934
+
935
+ plt.tight_layout()
936
+ if save_plot:
937
+ plt.savefig(plot_name, dpi=300)
938
+ plt.show()
939
+ return None
940
+
941
+
942
+ def plot_frap_quantification_all_images(df_tracking_all, save_plot=False, plot_name='temp.png'):
943
+ # Create a figure and two horizontal subplots
944
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
945
+ # Plot from the dataframe the mean_roi_frap_normalized vs the frame for each image_name
946
+ for name in df_tracking_all['image_name'].unique():
947
+ df_selected = df_tracking_all[df_tracking_all['image_name'] == name]
948
+ ax1.plot(df_selected['frame'], df_selected['mean_roi_frap_normalized'], label=name)
949
+ ax1.set_title('Normalized ROI FRAP Mean by Image')
950
+ ax1.set_xlabel('Frame')
951
+ ax1.set_ylabel('Normalized Mean ROI FRAP')
952
+ # Plot the mean and std of the mean_roi_frap_normalized vs frame for all df_tracking_all
953
+ mean_values = []
954
+ std_values = []
955
+ frames = df_tracking_all['frame'].unique()
956
+ for frame in frames:
957
+ df_selected = df_tracking_all[df_tracking_all['frame'] == frame]
958
+ mean_values.append(np.mean(df_selected['mean_roi_frap_normalized']))
959
+ std_values.append(np.std(df_selected['mean_roi_frap_normalized']))
960
+ ax2.errorbar(frames, mean_values, yerr=std_values, fmt='-o')
961
+ ax2.set_title('Mean and Standard Deviation of Normalized ROI FRAP')
962
+ ax2.set_xlabel('Frame')
963
+ ax2.set_ylabel('Mean ± STD')
964
+ # Display the plot
965
+ plt.tight_layout()
966
+ if save_plot:
967
+ plt.savefig(plot_name, dpi=300)
968
+ plt.show()
969
+ return np.array(frames), np.array(mean_values), np.array(std_values)
970
+
971
+
972
+ def create_pdf(list_combined_image_paths,pdf_name, remove_original_images=False):
973
+ pdf = FPDF()
974
+ pdf.set_auto_page_break(auto=True, margin=15)
975
+ pdf.set_font("Arial", size=12)
976
+ for image_path in list_combined_image_paths:
977
+ # if image_path exists, add it to the pdf
978
+ if image_path.exists():
979
+ pdf.add_page()
980
+ pdf.image(str(image_path), x=10, y=10, w=190)
981
+ # add page and text to the pdf indicating that the image does not exist
982
+ else:
983
+ pdf.add_page()
984
+ pdf.set_xy(10, 10)
985
+ pdf.set_font("Arial", size=12)
986
+ pdf.cell(0, 10, f"Image {image_path} was not processed. ", ln=True)
987
+ if remove_original_images:
988
+ image_path.unlink()
989
+ # save the pdf
990
+ pdf.output(str(pdf_name))
991
+ return None
992
+
993
+
994
+ def plot_t_half_values(df_fit, r2_threshold=0.5, suptitle=None,save_plot=True, plot_name='temp.png'):
995
+ """
996
+ Plot the half-time values (t_half_single, t_half_double_1st_process, t_half_double_2nd_process) as box plots.
997
+
998
+ Parameters:
999
+ - df_fit (pd.DataFrame): DataFrame containing the fit results.
1000
+ - r2_threshold (float): Minimum R^2 value to filter data. Default is 0.5.
1001
+ - y_lim_max (float or None): Maximum y-limit for the plots. If None, it will be set to the 95th percentile of the data.
1002
+ """
1003
+ # Remove rows where r_2_single is less than the threshold
1004
+ df_fit_selected = df_fit[df_fit['r_2_single'] > r2_threshold]
1005
+ # Concatenate t_half values into a single DataFrame
1006
+ tau_values = pd.concat([
1007
+ df_fit_selected['t_half_single'].dropna(),
1008
+ df_fit_selected['t_half_double_1st_process'].dropna(),
1009
+ df_fit_selected['t_half_double_2nd_process'].dropna(),
1010
+ ], axis=1)
1011
+ # Ensure that the columns are numeric
1012
+ tau_values = tau_values.apply(pd.to_numeric, errors='coerce')
1013
+ # Create the subplots
1014
+ fig, axes = plt.subplots(1, 3, figsize=(12, 5))
1015
+ # List of tau columns and labels
1016
+ tau_columns = ['t_half_single', 't_half_double_1st_process', 't_half_double_2nd_process']
1017
+ labels = [r"$t_{1/2}$"+ " [single exponential]", r"$t_{1/2}$"+ " 1st process [double exponential]", r"$t_{1/2}$"+ " 2nd process [double exponential]"]
1018
+ for i, col in enumerate(tau_columns):
1019
+ axes[i].boxplot(tau_values[col].dropna(), labels=[labels[i]])
1020
+ # Show all the values as dots
1021
+ axes[i].plot(np.ones(len(tau_values[col]))*1, tau_values[col], 'ro', alpha=0.3)
1022
+ #axes[i].set_ylim([0, y_lim_max])
1023
+ axes[i].set_ylabel(r"$t_{1/2}$")
1024
+ #axes[i].set_title(f'Box Plot of {labels[i]}')
1025
+ if suptitle is not None:
1026
+ fig.suptitle(suptitle, fontsize=16)
1027
+ if save_plot:
1028
+ plt.savefig(plot_name, dpi=300)
1029
+ plt.tight_layout()
1030
+ plt.show()
1031
+
1032
+
1033
+
1034
+
1035
+
1036
+ ##################################################################################
1037
+ ######################### Fit functions ##########################################
1038
+ ##################################################################################
1039
+
1040
+
1041
+
1042
+ def fit_model_to_frap(time, intensity, frap_time,suptitle=None, show_time_before_bleaching=True, save_plot=True, plot_name='temp.png'):
1043
+ # intial guesses for the single and double exponential models
1044
+ # p0_single = [intensity.max(), # max intensity
1045
+ # (intensity[-1] - np.min(intensity) ) / (np.max(intensity) - np.min(intensity)), # difference between max and min intensity
1046
+ # 1.0 / (time.max() - time.min())] # rate constant
1047
+ # time and intensity before the bleach
1048
+ time_before = time[:frap_time]
1049
+ intensity_before = intensity[:frap_time]
1050
+ # time and intensity after the bleach
1051
+ time = time[frap_time:]
1052
+ intensity = intensity[frap_time:]
1053
+ # Define the single and double exponential models
1054
+ def frap_model_single_exp(t,I_0, a, b ):
1055
+ # # i_fit = I_0 - a * exp(-b t)
1056
+ return I_0 - (a * np.exp(-b * t))
1057
+ #def frap_model_single_exp(t, I_0, I_inf, tau):
1058
+ # return I_0 + (I_inf - I_0) * (1 - np.exp(-tau * t))
1059
+ def frap_model_double_exp(t, I_0, a, b, g, d):
1060
+ # i_fit = I_0 - a * exp(-b t) - g * exp(-d t)
1061
+ return I_0 - (a * np.exp(-b * t)) - (g * np.exp(-d * t))
1062
+ # Initial guesses for parameters
1063
+ p0_single = [1, 0.5, 0.005]
1064
+ p0_double = [1, 0.5, 0.005, 1, 0.5]#, 50, 100, intensity.min()]
1065
+ # Fit the models
1066
+ flag_single = False
1067
+ flag_double = False
1068
+ try:
1069
+ params_single, _ = curve_fit(frap_model_single_exp, time, intensity, p0=p0_single)
1070
+ flag_single = True
1071
+ t_half_single = np.log(2)/params_single[2]
1072
+ except:
1073
+ params_single = [np.nan, np.nan, np.nan]
1074
+ t_half_single = np.nan
1075
+ try:
1076
+ params_double, _ = curve_fit(frap_model_double_exp, time, intensity, p0=p0_double)
1077
+ flag_double = True
1078
+ t_half_double_1st_process = np.log(2)/params_double[2]
1079
+ t_half_double_2nd_process = np.log(2)/params_double[4]
1080
+ except:
1081
+ params_double = [np.nan, np.nan, np.nan, np.nan, np.nan]
1082
+ t_half_double_1st_process = np.nan
1083
+ t_half_double_2nd_process = np.nan
1084
+ # Calculate R-squared for each model
1085
+ def compute_r_squared(data, fit):
1086
+ residuals = data - fit
1087
+ ss_res = np.sum(residuals**2)
1088
+ ss_tot = np.sum((data - np.mean(data))**2)
1089
+ return 1 - (ss_res / ss_tot)
1090
+ if flag_single:
1091
+ r_squared_single = compute_r_squared(intensity, frap_model_single_exp(time, *params_single))
1092
+ else:
1093
+ r_squared_single = np.nan
1094
+ if flag_double:
1095
+ r_squared_double = compute_r_squared(intensity, frap_model_double_exp(time, *params_double))
1096
+ else:
1097
+ r_squared_double = np.nan
1098
+ # Create plots
1099
+ fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 4))
1100
+ # Single exponential fit plot
1101
+ if show_time_before_bleaching:
1102
+ axes[0].plot(time_before, intensity_before, 'ro')
1103
+ axes[0].plot(time, intensity, 'ro', label='Data')
1104
+ if flag_single:
1105
+ axes[0].plot(time, frap_model_single_exp(time, *params_single), 'k-', label='Fit: Single Exp')
1106
+ axes[0].set_title('Single Exponential Fit')
1107
+ axes[0].set_xlabel('Time')
1108
+ axes[0].set_ylabel('Intensity')
1109
+ axes[0].legend()
1110
+ if flag_single:
1111
+ axes[0].text(0.1, 0.9, f"$t = {t_half_single:.2f}$", transform=axes[0].transAxes)
1112
+ axes[0].text(0.1, 0.8, f"$R^2 = {r_squared_single:.2f}$", transform=axes[0].transAxes)
1113
+ # Double exponential fit plot
1114
+ if show_time_before_bleaching:
1115
+ axes[1].plot(time_before, intensity_before, 'ro')
1116
+ axes[1].plot(time, intensity, 'ro', label='Data')
1117
+ if flag_double:
1118
+ axes[1].plot(time, frap_model_double_exp(time, *params_double), 'k-', label='Fit: Double Exp')
1119
+ axes[1].set_title('Double Exponential Fit')
1120
+ axes[1].set_xlabel('Time')
1121
+ axes[1].set_ylabel('Intensity')
1122
+ axes[1].legend()
1123
+ if flag_double:
1124
+ axes[1].text(0.1, 0.9, f"$t_{{1st}} = {t_half_double_1st_process:.2f}$", transform=axes[1].transAxes)
1125
+ axes[1].text(0.1, 0.8, f"$t_{{2nd}} = {t_half_double_2nd_process:.2f}$", transform=axes[1].transAxes)
1126
+ axes[1].text(0.1, 0.7, f"$R^2 = {r_squared_double:.2f}$", transform=axes[1].transAxes)
1127
+ if suptitle is not None:
1128
+ fig.suptitle(suptitle, fontsize=16)
1129
+ plt.tight_layout()
1130
+ if save_plot:
1131
+ plt.savefig(plot_name, dpi=300)
1132
+ plt.show()
1133
+ return t_half_single, t_half_double_1st_process,t_half_double_2nd_process, r_squared_single, r_squared_double
1134
+
1135
+
1136
+ def fit_model_to_frap_immobile_fraction(time, intensity, frap_time,suptitle=None, show_time_before_bleaching=True,save_plot=True, plot_name='temp.png'):
1137
+ # time and intensity before the bleach
1138
+ time_before = time[:frap_time]
1139
+ intensity_before = intensity[:frap_time]
1140
+ # time and intensity after the bleach
1141
+ time = time[frap_time:]
1142
+ intensity = intensity[frap_time:]
1143
+ # Define the single and double exponential models
1144
+ def frap_model_single_exp(t, f_imm, tau, I_0):
1145
+ return (1 - f_imm) * (1 - np.exp(-t / tau)) + I_0 * f_imm
1146
+ def frap_model_double_exp(t, f_imm, tau1, tau2, I_0):
1147
+ return (1 - f_imm) * (1 - np.exp(-t / tau1)) + f_imm * (1 - np.exp(-t / tau2)) + I_0
1148
+ # Initial guesses for parameters
1149
+ p0_single = [1-intensity.max(), 50, intensity.min()]
1150
+ p0_double = [1-intensity.max(), 50, 5, intensity.min()]
1151
+ # Fit the models
1152
+ flag_single = False
1153
+ flag_double = False
1154
+ try:
1155
+ params_single, _ = curve_fit(frap_model_single_exp, time, intensity, p0=p0_single)
1156
+ flag_single = True
1157
+ t_half_single = params_single[1]
1158
+
1159
+ if t_half_single < 0 or t_half_single > 2000:
1160
+ flag_single = True
1161
+ t_half_single = np.nan
1162
+ params_single = [np.nan, np.nan, np.nan]
1163
+ except:
1164
+ params_single = [np.nan, np.nan, np.nan]
1165
+ t_half_single = np.nan
1166
+ try:
1167
+ params_double, _ = curve_fit(frap_model_double_exp, time, intensity, p0=p0_double)
1168
+ flag_double = True
1169
+ t_half_double_1st_process = params_double[1]
1170
+ t_half_double_2nd_process = params_double[2]
1171
+ except:
1172
+ params_double = [np.nan, np.nan, np.nan, np.nan]
1173
+ t_half_double_1st_process = np.nan
1174
+ t_half_double_2nd_process = np.nan
1175
+ # Calculate R-squared for each model
1176
+ def compute_r_squared(data, fit):
1177
+ residuals = data - fit
1178
+ ss_res = np.sum(residuals**2)
1179
+ ss_tot = np.sum((data - np.mean(data))**2)
1180
+ return 1 - (ss_res / ss_tot)
1181
+ if flag_single:
1182
+ r_squared_single = compute_r_squared(intensity, frap_model_single_exp(time, *params_single))
1183
+ else:
1184
+ r_squared_single = np.nan
1185
+ if flag_double:
1186
+ r_squared_double = compute_r_squared(intensity, frap_model_double_exp(time, *params_double))
1187
+ else:
1188
+ r_squared_double = np.nan
1189
+ # Create plots
1190
+ fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 4))
1191
+ # Single exponential fit plot
1192
+ axes[0].plot(time, intensity, 'ro', label='Data')
1193
+ if show_time_before_bleaching:
1194
+ axes[0].plot(time_before, intensity_before, 'ro')
1195
+ if flag_single:
1196
+ axes[0].plot(time, frap_model_single_exp(time, *params_single), 'k-', label='Fit: Single Exp')
1197
+ axes[0].set_title('Single Exponential Fit')
1198
+ axes[0].set_xlabel('Time')
1199
+ axes[0].set_ylabel('Intensity')
1200
+ axes[0].legend()
1201
+ if flag_single:
1202
+ axes[0].text(0.1, 0.9, f"$t = {t_half_single:.2f}$", transform=axes[0].transAxes)
1203
+ axes[0].text(0.1, 0.8, f"$R^2 = {r_squared_single:.2f}$", transform=axes[0].transAxes)
1204
+ # Double exponential fit plot
1205
+ axes[1].plot(time, intensity, 'ro', label='Data')
1206
+ if show_time_before_bleaching:
1207
+ axes[1].plot(time_before, intensity_before, 'ro')
1208
+ if flag_double:
1209
+ axes[1].plot(time, frap_model_double_exp(time, *params_double), 'k-', label='Fit: Double Exp')
1210
+ axes[1].set_title('Double Exponential Fit')
1211
+ axes[1].set_xlabel('Time')
1212
+ axes[1].set_ylabel('Intensity')
1213
+ axes[1].legend()
1214
+ if flag_double:
1215
+ axes[1].text(0.1, 0.9, f"$t_{{1st}} = {t_half_double_1st_process:.2f}$", transform=axes[1].transAxes)
1216
+ axes[1].text(0.1, 0.8, f"$t_{{2nd}} = {t_half_double_2nd_process:.2f}$", transform=axes[1].transAxes)
1217
+ axes[1].text(0.1, 0.7, f"$R^2 = {r_squared_double:.2f}$", transform=axes[1].transAxes)
1218
+ plt.tight_layout()
1219
+ if suptitle is not None:
1220
+ fig.suptitle(suptitle, fontsize=16)
1221
+ plt.tight_layout()
1222
+ if save_plot:
1223
+ plt.savefig(plot_name, dpi=300)
1224
+ plt.show()
1225
+ return t_half_single, t_half_double_1st_process,t_half_double_2nd_process, r_squared_single, r_squared_double