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.
- microlive/__init__.py +50 -0
- microlive/data/__init__.py +0 -0
- microlive/data/icons/__init__.py +0 -0
- microlive/data/icons/icon_micro.png +0 -0
- microlive/data/models/__init__.py +0 -0
- microlive/gui/__init__.py +1 -0
- microlive/gui/app.py +16340 -0
- microlive/gui/main.py +86 -0
- microlive/gui/micro_mac.command +18 -0
- microlive/gui/micro_windows.bat +24 -0
- microlive/imports.py +209 -0
- microlive/microscopy.py +13321 -0
- microlive/ml_spot_detection.py +252 -0
- microlive/pipelines/__init__.py +17 -0
- microlive/pipelines/pipeline_FRAP.py +1225 -0
- microlive/pipelines/pipeline_folding_efficiency.py +297 -0
- microlive/pipelines/pipeline_particle_tracking.py +489 -0
- microlive/pipelines/pipeline_spot_detection_no_tracking.py +368 -0
- microlive/utils/__init__.py +13 -0
- microlive/utils/device.py +99 -0
- microlive/utils/resources.py +60 -0
- microlive-1.0.11.dist-info/METADATA +361 -0
- microlive-1.0.11.dist-info/RECORD +26 -0
- microlive-1.0.11.dist-info/WHEEL +4 -0
- microlive-1.0.11.dist-info/entry_points.txt +2 -0
- microlive-1.0.11.dist-info/licenses/LICENSE +674 -0
|
@@ -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
|