spacr 0.0.1__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.
spacr/measure.py ADDED
@@ -0,0 +1,1014 @@
1
+ import os, cv2, time, sqlite3, traceback, shutil
2
+ import numpy as np
3
+ import pandas as pd
4
+ from collections import defaultdict
5
+ from scipy.stats import pearsonr
6
+ import matplotlib as mpl
7
+ import multiprocessing as mp
8
+ from scipy.ndimage import distance_transform_edt, generate_binary_structure
9
+ from skimage.measure import regionprops, regionprops_table, shannon_entropy
10
+ from skimage.exposure import rescale_intensity
11
+ from scipy.ndimage import binary_dilation
12
+ from skimage.segmentation import find_boundaries
13
+ from skimage.feature import graycomatrix, graycoprops
14
+ from mahotas.features import zernike_moments
15
+
16
+ from .logger import log_function_call
17
+
18
+ #from .io import create_database, _save_settings_to_db
19
+ #from .timelapse import _timelapse_masks_to_gif, _scmovie
20
+ #from .plot import _plot_cropped_arrays, _save_scimg_plot
21
+ #from .utils import _merge_overlapping_objects, _filter_object, _relabel_parent_with_child_labels, _exclude_objects
22
+ #from .utils import _merge_and_save_to_database, _crop_center, _find_bounding_box, _generate_names, _get_percentiles, normalize_to_dtype, _map_wells_png, _list_endpoint_subdirectories, _generate_representative_images
23
+
24
+ def get_components(cell_mask, nucleus_mask, pathogen_mask):
25
+ """
26
+ Get the components (nucleus and pathogens) for each cell in the given masks.
27
+
28
+ Args:
29
+ cell_mask (ndarray): Binary mask of cell labels.
30
+ nucleus_mask (ndarray): Binary mask of nucleus labels.
31
+ pathogen_mask (ndarray): Binary mask of pathogen labels.
32
+
33
+ Returns:
34
+ tuple: A tuple containing two dataframes - nucleus_df and pathogen_df.
35
+ nucleus_df (DataFrame): Dataframe with columns 'cell_id' and 'nucleus',
36
+ representing the mapping of each cell to its nucleus.
37
+ pathogen_df (DataFrame): Dataframe with columns 'cell_id' and 'pathogen',
38
+ representing the mapping of each cell to its pathogens.
39
+ """
40
+ # Create mappings from each cell to its nucleus, pathogens, and cytoplasms
41
+ cell_to_nucleus = defaultdict(list)
42
+ cell_to_pathogen = defaultdict(list)
43
+ # Get unique cell labels
44
+ cell_labels = np.unique(cell_mask)
45
+ # Iterate over each cell label
46
+ for cell_id in cell_labels:
47
+ if cell_id == 0:
48
+ continue
49
+ # Find corresponding component labels
50
+ nucleus_ids = np.unique(nucleus_mask[cell_mask == cell_id])
51
+ pathogen_ids = np.unique(pathogen_mask[cell_mask == cell_id])
52
+ # Update dictionaries, ignoring 0 (background) labels
53
+ cell_to_nucleus[cell_id] = nucleus_ids[nucleus_ids != 0].tolist()
54
+ cell_to_pathogen[cell_id] = pathogen_ids[pathogen_ids != 0].tolist()
55
+ # Convert dictionaries to dataframes
56
+ nucleus_df = pd.DataFrame(list(cell_to_nucleus.items()), columns=['cell_id', 'nucleus'])
57
+ pathogen_df = pd.DataFrame(list(cell_to_pathogen.items()), columns=['cell_id', 'pathogen'])
58
+ # Explode lists
59
+ nucleus_df = nucleus_df.explode('nucleus')
60
+ pathogen_df = pathogen_df.explode('pathogen')
61
+ return nucleus_df, pathogen_df
62
+
63
+ def _calculate_zernike(mask, df, degree=8):
64
+ """
65
+ Calculate Zernike moments for each region in the given mask image.
66
+
67
+ Args:
68
+ mask (ndarray): Binary mask image.
69
+ df (DataFrame): Input DataFrame.
70
+ degree (int, optional): Degree of Zernike moments. Defaults to 8.
71
+
72
+ Returns:
73
+ DataFrame: Updated DataFrame with Zernike features appended, if any regions are found in the mask.
74
+ Otherwise, returns the original DataFrame.
75
+ Raises:
76
+ ValueError: If the lengths of Zernike moments are not consistent.
77
+
78
+ """
79
+ zernike_features = []
80
+ for region in regionprops(mask):
81
+ zernike_moment = zernike_moments(region.image, degree)
82
+ zernike_features.append(zernike_moment.tolist())
83
+
84
+ if zernike_features:
85
+ feature_length = len(zernike_features[0])
86
+ for feature in zernike_features:
87
+ if len(feature) != feature_length:
88
+ raise ValueError("All Zernike moments must be of the same length")
89
+
90
+ zernike_df = pd.DataFrame(zernike_features, columns=[f'zernike_{i}' for i in range(feature_length)])
91
+ return pd.concat([df.reset_index(drop=True), zernike_df], axis=1)
92
+ else:
93
+ return df
94
+
95
+ def _morphological_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, settings, zernike=True, degree=8):
96
+ """
97
+ Calculate morphological measurements for cells, nucleus, pathogens, and cytoplasms based on the given masks.
98
+
99
+ Args:
100
+ cell_mask (ndarray): Binary mask of cell labels.
101
+ nucleus_mask (ndarray): Binary mask of nucleus labels.
102
+ pathogen_mask (ndarray): Binary mask of pathogen labels.
103
+ cytoplasm_mask (ndarray): Binary mask of cytoplasm labels.
104
+ settings (dict): Dictionary containing settings for the measurements.
105
+ zernike (bool, optional): Flag indicating whether to calculate Zernike moments. Defaults to True.
106
+ degree (int, optional): Degree of Zernike moments. Defaults to 8.
107
+
108
+ Returns:
109
+ tuple: A tuple containing four dataframes - cell_df, nucleus_df, pathogen_df, and cytoplasm_df.
110
+ cell_df (DataFrame): Dataframe with morphological measurements for cells.
111
+ nucleus_df (DataFrame): Dataframe with morphological measurements for nucleus.
112
+ pathogen_df (DataFrame): Dataframe with morphological measurements for pathogens.
113
+ cytoplasm_df (DataFrame): Dataframe with morphological measurements for cytoplasms.
114
+ """
115
+ morphological_props = ['label', 'area', 'area_filled', 'area_bbox', 'convex_area', 'major_axis_length', 'minor_axis_length',
116
+ 'eccentricity', 'solidity', 'extent', 'perimeter', 'euler_number', 'equivalent_diameter_area', 'feret_diameter_max']
117
+
118
+ prop_ls = []
119
+ ls = []
120
+
121
+ # Create mappings from each cell to its nucleus, pathogens, and cytoplasms
122
+ if settings['cell_mask_dim'] is not None:
123
+ cell_to_nucleus, cell_to_pathogen = get_components(cell_mask, nucleus_mask, pathogen_mask)
124
+ cell_props = pd.DataFrame(regionprops_table(cell_mask, properties=morphological_props))
125
+ cell_props = _calculate_zernike(cell_mask, cell_props, degree=degree)
126
+ prop_ls = prop_ls + [cell_props]
127
+ ls = ls + ['cell']
128
+ else:
129
+ prop_ls = prop_ls + [pd.DataFrame()]
130
+ ls = ls + ['cell']
131
+
132
+ if settings['nucleus_mask_dim'] is not None:
133
+ nucleus_props = pd.DataFrame(regionprops_table(nucleus_mask, properties=morphological_props))
134
+ nucleus_props = _calculate_zernike(nucleus_mask, nucleus_props, degree=degree)
135
+ if settings['cell_mask_dim'] is not None:
136
+ nucleus_props = pd.merge(nucleus_props, cell_to_nucleus, left_on='label', right_on='nucleus', how='left')
137
+ prop_ls = prop_ls + [nucleus_props]
138
+ ls = ls + ['nucleus']
139
+ else:
140
+ prop_ls = prop_ls + [pd.DataFrame()]
141
+ ls = ls + ['nucleus']
142
+
143
+ if settings['pathogen_mask_dim'] is not None:
144
+ pathogen_props = pd.DataFrame(regionprops_table(pathogen_mask, properties=morphological_props))
145
+ pathogen_props = _calculate_zernike(pathogen_mask, pathogen_props, degree=degree)
146
+ if settings['cell_mask_dim'] is not None:
147
+ pathogen_props = pd.merge(pathogen_props, cell_to_pathogen, left_on='label', right_on='pathogen', how='left')
148
+ prop_ls = prop_ls + [pathogen_props]
149
+ ls = ls + ['pathogen']
150
+ else:
151
+ prop_ls = prop_ls + [pd.DataFrame()]
152
+ ls = ls + ['pathogen']
153
+
154
+ if settings['cytoplasm']:
155
+ cytoplasm_props = pd.DataFrame(regionprops_table(cytoplasm_mask, properties=morphological_props))
156
+ prop_ls = prop_ls + [cytoplasm_props]
157
+ ls = ls + ['cytoplasm']
158
+ else:
159
+ prop_ls = prop_ls + [pd.DataFrame()]
160
+ ls = ls + ['cytoplasm']
161
+
162
+ df_ls = []
163
+ for i,df in enumerate(prop_ls):
164
+ df.columns = [f'{ls[i]}_{col}' for col in df.columns]
165
+ df = df.rename(columns={col: 'label' for col in df.columns if 'label' in col})
166
+ df_ls.append(df)
167
+
168
+ return df_ls[0], df_ls[1], df_ls[2], df_ls[3]
169
+
170
+ def _create_dataframe(radial_distributions, object_type):
171
+ """
172
+ Create a pandas DataFrame from the given radial distributions.
173
+
174
+ Parameters:
175
+ - radial_distributions (dict): A dictionary containing the radial distributions.
176
+ - object_type (str): The type of object.
177
+
178
+ Returns:
179
+ - df (pandas.DataFrame): The created DataFrame.
180
+ """
181
+ df = pd.DataFrame()
182
+ for key, value in radial_distributions.items():
183
+ cell_label, object_label, channel_index = key
184
+ for i in range(len(value)):
185
+ col_name = f'{object_type}_rad_dist_channel_{channel_index}_bin_{i}'
186
+ df.loc[object_label, col_name] = value[i]
187
+ df.loc[object_label, 'cell_id'] = cell_label
188
+ # Reset the index and rename the column that was previously the index
189
+ df = df.reset_index().rename(columns={'index': 'label'})
190
+ return df
191
+
192
+ def _extended_regionprops_table(labels, image, intensity_props):
193
+ """
194
+ Calculate extended region properties table.
195
+
196
+ Args:
197
+ labels (ndarray): Labeled image.
198
+ image (ndarray): Input image.
199
+ intensity_props (list): List of intensity properties to calculate.
200
+
201
+ Returns:
202
+ DataFrame: Extended region properties table.
203
+
204
+ """
205
+ regions = regionprops(labels, image)
206
+ props = regionprops_table(labels, image, properties=intensity_props)
207
+ percentiles = [5, 10, 25, 50, 75, 85, 95]
208
+ for p in percentiles:
209
+ props[f'percentile_{p}'] = [
210
+ np.percentile(region.intensity_image.flatten()[~np.isnan(region.intensity_image.flatten())], p)
211
+ for region in regions]
212
+ return pd.DataFrame(props)
213
+
214
+ def _calculate_homogeneity(label, channel, distances=[2,4,8,16,32,64]):
215
+ """
216
+ Calculate the homogeneity values for each region in the label mask.
217
+
218
+ Parameters:
219
+ - label (ndarray): The label mask containing the regions.
220
+ - channel (ndarray): The image channel corresponding to the label mask.
221
+ - distances (list): The distances to calculate the homogeneity for.
222
+
223
+ Returns:
224
+ - homogeneity_df (DataFrame): A DataFrame containing the homogeneity values for each region and distance.
225
+ """
226
+ homogeneity_values = []
227
+ # Iterate through the regions in label_mask
228
+ for region in regionprops(label):
229
+ region_image = (region.image * channel[region.slice]).astype(int)
230
+ homogeneity_per_distance = []
231
+ for d in distances:
232
+ rescaled_image = rescale_intensity(region_image, out_range=(0, 255)).astype('uint8')
233
+ glcm = graycomatrix(rescaled_image, [d], [0], symmetric=True, normed=True)
234
+ homogeneity_per_distance.append(graycoprops(glcm, 'homogeneity')[0, 0])
235
+ homogeneity_values.append(homogeneity_per_distance)
236
+ columns = [f'homogeneity_distance_{d}' for d in distances]
237
+ homogeneity_df = pd.DataFrame(homogeneity_values, columns=columns)
238
+
239
+ return homogeneity_df
240
+
241
+ def _periphery_intensity(label_mask, image):
242
+ """
243
+ Calculate intensity statistics for the periphery regions in the label mask.
244
+
245
+ Args:
246
+ label_mask (ndarray): Binary mask indicating the regions of interest.
247
+ image (ndarray): Input image.
248
+
249
+ Returns:
250
+ list: List of tuples containing periphery intensity statistics for each region.
251
+ Each tuple contains the region label and the following statistics:
252
+ - Mean intensity
253
+ - 5th percentile intensity
254
+ - 10th percentile intensity
255
+ - 25th percentile intensity
256
+ - 50th percentile intensity (median)
257
+ - 75th percentile intensity
258
+ - 85th percentile intensity
259
+ - 95th percentile intensity
260
+ """
261
+ periphery_intensity_stats = []
262
+ boundary = find_boundaries(label_mask)
263
+ for region in np.unique(label_mask)[1:]: # skip the background label
264
+ region_boundary = boundary & (label_mask == region)
265
+ intensities = image[region_boundary]
266
+ if intensities.size == 0:
267
+ periphery_intensity_stats.append((region, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan))
268
+ else:
269
+ periphery_intensity_stats.append((region, np.mean(intensities), np.percentile(intensities,5), np.percentile(intensities,10),
270
+ np.percentile(intensities,25), np.percentile(intensities,50),
271
+ np.percentile(intensities,75), np.percentile(intensities,85),
272
+ np.percentile(intensities,95)))
273
+ return periphery_intensity_stats
274
+
275
+ def _outside_intensity(label_mask, image, distance=5):
276
+ """
277
+ Calculate the statistics of intensities outside each labeled region in the image.
278
+
279
+ Args:
280
+ label_mask (ndarray): Binary mask indicating the labeled regions.
281
+ image (ndarray): Input image.
282
+ distance (int): Distance for dilation operation (default: 5).
283
+
284
+ Returns:
285
+ list: List of tuples containing the statistics for each labeled region.
286
+ Each tuple contains the region label and the following statistics:
287
+ - Mean intensity
288
+ - 5th percentile intensity
289
+ - 10th percentile intensity
290
+ - 25th percentile intensity
291
+ - 50th percentile intensity (median)
292
+ - 75th percentile intensity
293
+ - 85th percentile intensity
294
+ - 95th percentile intensity
295
+ """
296
+ outside_intensity_stats = []
297
+ for region in np.unique(label_mask)[1:]: # skip the background label
298
+ region_mask = label_mask == region
299
+ dilated_mask = binary_dilation(region_mask, iterations=distance)
300
+ outside_mask = dilated_mask & ~region_mask
301
+ intensities = image[outside_mask]
302
+ if intensities.size == 0:
303
+ outside_intensity_stats.append((region, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan))
304
+ else:
305
+ outside_intensity_stats.append((region, np.mean(intensities), np.percentile(intensities,5), np.percentile(intensities,10),
306
+ np.percentile(intensities,25), np.percentile(intensities,50),
307
+ np.percentile(intensities,75), np.percentile(intensities,85),
308
+ np.percentile(intensities,95)))
309
+ return outside_intensity_stats
310
+
311
+ def _calculate_radial_distribution(cell_mask, object_mask, channel_arrays, num_bins=6):
312
+ """
313
+ Calculate the radial distribution of average intensities for each object in each cell.
314
+
315
+ Args:
316
+ cell_mask (numpy.ndarray): The mask representing the cells.
317
+ object_mask (numpy.ndarray): The mask representing the objects.
318
+ channel_arrays (numpy.ndarray): The array of channel images.
319
+ num_bins (int, optional): The number of bins for the radial distribution. Defaults to 6.
320
+
321
+ Returns:
322
+ dict: A dictionary containing the radial distributions of average intensities for each object in each cell.
323
+ The keys are tuples of (cell_label, object_label, channel_index), and the values are numpy arrays
324
+ representing the radial distributions.
325
+
326
+ """
327
+ def _calculate_average_intensity(distance_map, single_channel_image, num_bins):
328
+ """
329
+ Calculate the average intensity of a single-channel image based on the distance map.
330
+
331
+ Args:
332
+ distance_map (numpy.ndarray): The distance map.
333
+ single_channel_image (numpy.ndarray): The single-channel image.
334
+ num_bins (int): The number of bins for the radial distribution.
335
+
336
+ Returns:
337
+ numpy.ndarray: The radial distribution of average intensities.
338
+
339
+ """
340
+ radial_distribution = np.zeros(num_bins)
341
+ for i in range(num_bins):
342
+ min_distance = i * (distance_map.max() / num_bins)
343
+ max_distance = (i + 1) * (distance_map.max() / num_bins)
344
+ bin_mask = (distance_map >= min_distance) & (distance_map < max_distance)
345
+ radial_distribution[i] = single_channel_image[bin_mask].mean()
346
+ return radial_distribution
347
+
348
+
349
+ object_radial_distributions = {}
350
+
351
+ # get unique cell labels
352
+ cell_labels = np.unique(cell_mask)
353
+ cell_labels = cell_labels[cell_labels != 0]
354
+
355
+ for cell_label in cell_labels:
356
+ cell_region = cell_mask == cell_label
357
+
358
+ object_labels = np.unique(object_mask[cell_region])
359
+ object_labels = object_labels[object_labels != 0]
360
+
361
+ for object_label in object_labels:
362
+ objecyt_region = object_mask == object_label
363
+ object_boundary = find_boundaries(objecyt_region, mode='outer')
364
+ distance_map = distance_transform_edt(~object_boundary) * cell_region
365
+ for channel_index in range(channel_arrays.shape[2]):
366
+ radial_distribution = _calculate_average_intensity(distance_map, channel_arrays[:, :, channel_index], num_bins)
367
+ object_radial_distributions[(cell_label, object_label, channel_index)] = radial_distribution
368
+
369
+ return object_radial_distributions
370
+
371
+ def _calculate_correlation_object_level(channel_image1, channel_image2, mask, settings):
372
+ """
373
+ Calculate correlation at the object level between two channel images based on a mask.
374
+
375
+ Args:
376
+ channel_image1 (numpy.ndarray): The first channel image.
377
+ channel_image2 (numpy.ndarray): The second channel image.
378
+ mask (numpy.ndarray): The mask indicating the objects.
379
+ settings (dict): Additional settings for correlation calculation.
380
+
381
+ Returns:
382
+ pandas.DataFrame: A DataFrame containing the correlation data at the object level.
383
+ """
384
+ thresholds = settings['manders_thresholds']
385
+
386
+ corr_data = {}
387
+ for i in np.unique(mask)[1:]:
388
+ object_mask = (mask == i)
389
+ object_channel_image1 = channel_image1[object_mask]
390
+ object_channel_image2 = channel_image2[object_mask]
391
+ total_intensity1 = np.sum(object_channel_image1)
392
+ total_intensity2 = np.sum(object_channel_image2)
393
+
394
+ if len(object_channel_image1) < 2 or len(object_channel_image2) < 2:
395
+ pearson_corr = np.nan
396
+ else:
397
+ pearson_corr, _ = pearsonr(object_channel_image1, object_channel_image2)
398
+
399
+ corr_data[i] = {f'label_correlation': i,
400
+ f'Pearson_correlation': pearson_corr}
401
+
402
+ for thresh in thresholds:
403
+ chan1_thresh = np.percentile(object_channel_image1, thresh)
404
+ chan2_thresh = np.percentile(object_channel_image2, thresh)
405
+
406
+ # boolean mask where both signals are present
407
+ overlap_mask = (object_channel_image1 > chan1_thresh) & (object_channel_image2 > chan2_thresh)
408
+ M1 = np.sum(object_channel_image1[overlap_mask]) / total_intensity1 if total_intensity1 > 0 else 0
409
+ M2 = np.sum(object_channel_image2[overlap_mask]) / total_intensity2 if total_intensity2 > 0 else 0
410
+
411
+ corr_data[i].update({f'M1_correlation_{thresh}': M1,
412
+ f'M2_correlation_{thresh}': M2})
413
+
414
+ return pd.DataFrame(corr_data.values())
415
+
416
+ def _estimate_blur(image):
417
+ """
418
+ Estimates the blur of an image by computing the variance of its Laplacian.
419
+
420
+ Parameters:
421
+ image (numpy.ndarray): The input image.
422
+
423
+ Returns:
424
+ float: The variance of the Laplacian of the image.
425
+ """
426
+ # Check if the image is not already in a floating-point format
427
+ if image.dtype != np.float32 and image.dtype != np.float64:
428
+ # Convert the image to float64 for processing
429
+ image_float = image.astype(np.float64)
430
+ else:
431
+ # If it's already a floating-point image, use it as is
432
+ image_float = image
433
+ # Compute the Laplacian of the image
434
+ lap = cv2.Laplacian(image_float, cv2.CV_64F)
435
+ # Compute and return the variance of the Laplacian
436
+ return lap.var()
437
+
438
+ def _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, channel_arrays, settings, sizes=[3, 6, 12, 24], periphery=True, outside=True):
439
+ """
440
+ Calculate various intensity measurements for different regions in the image.
441
+
442
+ Args:
443
+ cell_mask (ndarray): Binary mask indicating the cell regions.
444
+ nucleus_mask (ndarray): Binary mask indicating the nucleus regions.
445
+ pathogen_mask (ndarray): Binary mask indicating the pathogen regions.
446
+ cytoplasm_mask (ndarray): Binary mask indicating the cytoplasm regions.
447
+ channel_arrays (ndarray): Array of channel images.
448
+ settings (dict): Additional settings for the intensity measurements.
449
+ sizes (list, optional): List of sizes for the measurements. Defaults to [3, 6, 12, 24].
450
+ periphery (bool, optional): Flag indicating whether to calculate periphery intensity measurements. Defaults to True.
451
+ outside (bool, optional): Flag indicating whether to calculate outside intensity measurements. Defaults to True.
452
+
453
+ Returns:
454
+ dict: A dictionary containing the calculated intensity measurements.
455
+
456
+ """
457
+ radial_dist = settings['radial_dist']
458
+ calculate_correlation = settings['calculate_correlation']
459
+ homogeneity = settings['homogeneity']
460
+ distances = settings['homogeneity_distances']
461
+
462
+ intensity_props = ["label", "centroid_weighted", "centroid_weighted_local", "max_intensity", "mean_intensity", "min_intensity"]
463
+ col_lables = ['region_label', 'mean', '5_percentile', '10_percentile', '25_percentile', '50_percentile', '75_percentile', '85_percentile', '95_percentile']
464
+ cell_dfs, nucleus_dfs, pathogen_dfs, cytoplasm_dfs = [], [], [], []
465
+ ls = ['cell','nucleus','pathogen','cytoplasm']
466
+ labels = [cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask]
467
+ dfs = [cell_dfs, nucleus_dfs, pathogen_dfs, cytoplasm_dfs]
468
+
469
+ for i in range(0,channel_arrays.shape[-1]):
470
+ channel = channel_arrays[:, :, i]
471
+ for j, (label, df) in enumerate(zip(labels, dfs)):
472
+
473
+ if np.max(label) == 0:
474
+ empty_df = pd.DataFrame()
475
+ df.append(empty_df)
476
+ continue
477
+
478
+ mask_intensity_df = _extended_regionprops_table(label, channel, intensity_props)
479
+ mask_intensity_df['shannon_entropy'] = shannon_entropy(channel, base=2)
480
+
481
+ if homogeneity:
482
+ homogeneity_df = _calculate_homogeneity(label, channel, distances)
483
+ mask_intensity_df = pd.concat([mask_intensity_df.reset_index(drop=True), homogeneity_df], axis=1)
484
+
485
+ if periphery:
486
+ if ls[j] == 'nucleus' or ls[j] == 'pathogen':
487
+ periphery_intensity_stats = _periphery_intensity(label, channel)
488
+ mask_intensity_df = pd.concat([mask_intensity_df, pd.DataFrame(periphery_intensity_stats, columns=[f'periphery_{stat}' for stat in col_lables])],axis=1)
489
+
490
+ if outside:
491
+ if ls[j] == 'nucleus' or ls[j] == 'pathogen':
492
+ outside_intensity_stats = _outside_intensity(label, channel)
493
+ mask_intensity_df = pd.concat([mask_intensity_df, pd.DataFrame(outside_intensity_stats, columns=[f'outside_{stat}' for stat in col_lables])], axis=1)
494
+
495
+ blur_col = [_estimate_blur(channel[label == region_label]) for region_label in mask_intensity_df['label']]
496
+ mask_intensity_df[f'{ls[j]}_channel_{i}_blur'] = blur_col
497
+
498
+ mask_intensity_df.columns = [f'{ls[j]}_channel_{i}_{col}' if col != 'label' else col for col in mask_intensity_df.columns]
499
+ df.append(mask_intensity_df)
500
+
501
+ if radial_dist:
502
+ if np.max(nucleus_mask) != 0:
503
+ nucleus_radial_distributions = _calculate_radial_distribution(cell_mask, nucleus_mask, channel_arrays, num_bins=6)
504
+ nucleus_df = _create_dataframe(nucleus_radial_distributions, 'nucleus')
505
+ dfs[1].append(nucleus_df)
506
+
507
+ if np.max(nucleus_mask) != 0:
508
+ pathogen_radial_distributions = _calculate_radial_distribution(cell_mask, pathogen_mask, channel_arrays, num_bins=6)
509
+ pathogen_df = _create_dataframe(pathogen_radial_distributions, 'pathogen')
510
+ dfs[2].append(pathogen_df)
511
+
512
+ if calculate_correlation:
513
+ if channel_arrays.shape[-1] >= 2:
514
+ for i in range(channel_arrays.shape[-1]):
515
+ for j in range(i+1, channel_arrays.shape[-1]):
516
+ chan_i = channel_arrays[:, :, i]
517
+ chan_j = channel_arrays[:, :, j]
518
+ for m, mask in enumerate(labels):
519
+ coloc_df = _calculate_correlation_object_level(chan_i, chan_j, mask, settings)
520
+ coloc_df.columns = [f'{ls[m]}_channel_{i}_channel_{j}_{col}' for col in coloc_df.columns]
521
+ dfs[m].append(coloc_df)
522
+
523
+ return pd.concat(cell_dfs, axis=1), pd.concat(nucleus_dfs, axis=1), pd.concat(pathogen_dfs, axis=1), pd.concat(cytoplasm_dfs, axis=1)
524
+
525
+ @log_function_call
526
+ def _measure_crop_core(index, time_ls, file, settings):
527
+ """
528
+ Measure and crop the images based on specified settings.
529
+
530
+ Parameters:
531
+ - index: int
532
+ The index of the image.
533
+ - time_ls: list
534
+ The list of time points.
535
+ - file: str
536
+ The file path of the image.
537
+ - settings: dict
538
+ The dictionary containing the settings for measurement and cropping.
539
+
540
+ Returns:
541
+ - cropped_images: list
542
+ A list of cropped images.
543
+ """
544
+
545
+ from .io import _create_database
546
+ from .plot import _plot_cropped_arrays
547
+ from .utils import _merge_overlapping_objects, _filter_object, _relabel_parent_with_child_labels, _exclude_objects, normalize_to_dtype
548
+ from .utils import _merge_and_save_to_database, _crop_center, _find_bounding_box, _generate_names, _get_percentiles, _map_wells_png
549
+
550
+ start = time.time()
551
+ try:
552
+ source_folder = os.path.dirname(settings['input_folder'])
553
+ file_name = os.path.splitext(file)[0]
554
+ data = np.load(os.path.join(settings['input_folder'], file))
555
+
556
+ data_type = data.dtype
557
+ if settings['save_measurements']:
558
+ os.makedirs(source_folder+'/measurements', exist_ok=True)
559
+ _create_database(source_folder+'/measurements/measurements.db')
560
+
561
+ if settings['plot_filtration']:
562
+ _plot_cropped_arrays(data)
563
+
564
+ channel_arrays = data[:, :, settings['channels']].astype(data_type)
565
+ if settings['cell_mask_dim'] is not None:
566
+ cell_mask = data[:, :, settings['cell_mask_dim']].astype(data_type)
567
+
568
+ if settings['cell_min_size'] is not None and settings['cell_min_size'] != 0:
569
+ cell_mask = _filter_object(cell_mask, settings['cell_min_size'])
570
+ else:
571
+ cell_mask = np.zeros_like(data[:, :, 0])
572
+ settings['cytoplasm'] = False
573
+ settings['include_uninfected'] = True
574
+
575
+ if settings['nucleus_mask_dim'] is not None:
576
+ nucleus_mask = data[:, :, settings['nucleus_mask_dim']].astype(data_type)
577
+ if settings['cell_mask_dim'] is not None:
578
+ nucleus_mask, cell_mask = _merge_overlapping_objects(mask1=nucleus_mask, mask2=cell_mask)
579
+ if settings['nucleus_min_size'] is not None and settings['nucleus_min_size'] != 0:
580
+ nucleus_mask = _filter_object(nucleus_mask, settings['nucleus_min_size'])
581
+ if settings['timelapse_objects'] == 'nucleus':
582
+ if settings['cell_mask_dim'] is not None:
583
+ cell_mask, nucleus_mask = _relabel_parent_with_child_labels(cell_mask, nucleus_mask)
584
+ data[:, :, settings['cell_mask_dim']] = cell_mask
585
+ data[:, :, settings['nucleus_mask_dim']] = nucleus_mask
586
+ save_folder = settings['input_folder']
587
+ np.save(os.path.join(save_folder, file), data)
588
+
589
+ else:
590
+ nucleus_mask = np.zeros_like(data[:, :, 0])
591
+
592
+ if settings['pathogen_mask_dim'] is not None:
593
+ pathogen_mask = data[:, :, settings['pathogen_mask_dim']].astype(data_type)
594
+ if settings['merge_edge_pathogen_cells']:
595
+ if settings['cell_mask_dim'] is not None:
596
+ pathogen_mask, cell_mask = _merge_overlapping_objects(mask1=pathogen_mask, mask2=cell_mask)
597
+ if settings['pathogen_min_size'] is not None and settings['pathogen_min_size'] != 0:
598
+ pathogen_mask = _filter_object(pathogen_mask, settings['pathogen_min_size'])
599
+ else:
600
+ pathogen_mask = np.zeros_like(data[:, :, 0])
601
+
602
+ # Create cytoplasm mask
603
+ if settings['cytoplasm']:
604
+ if settings['cell_mask_dim'] is not None:
605
+ if settings['nucleus_mask_dim'] is not None and settings['pathogen_mask_dim'] is not None:
606
+ cytoplasm_mask = np.where(np.logical_or(nucleus_mask != 0, pathogen_mask != 0), 0, cell_mask)
607
+ elif settings['nucleus_mask_dim'] is not None:
608
+ cytoplasm_mask = np.where(nucleus_mask != 0, 0, cell_mask)
609
+ elif settings['pathogen_mask_dim'] is not None:
610
+ cytoplasm_mask = np.where(pathogen_mask != 0, 0, cell_mask)
611
+ else:
612
+ cytoplasm_mask = np.zeros_like(cell_mask)
613
+ else:
614
+ cytoplasm_mask = np.zeros_like(cell_mask)
615
+
616
+ if settings['cell_min_size'] is not None and settings['cell_min_size'] != 0:
617
+ cell_mask = _filter_object(cell_mask, settings['cell_min_size'])
618
+ if settings['nucleus_min_size'] is not None and settings['nucleus_min_size'] != 0:
619
+ nucleus_mask = _filter_object(nucleus_mask, settings['nucleus_min_size'])
620
+ if settings['pathogen_min_size'] is not None and settings['pathogen_min_size'] != 0:
621
+ pathogen_mask = _filter_object(pathogen_mask, settings['pathogen_min_size'])
622
+ if settings['cytoplasm_min_size'] is not None and settings['cytoplasm_min_size'] != 0:
623
+ cytoplasm_mask = _filter_object(cytoplasm_mask, settings['cytoplasm_min_size'])
624
+
625
+ if settings['cell_mask_dim'] is not None and settings['pathogen_mask_dim'] is not None:
626
+ if settings['include_uninfected'] == False:
627
+ cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask = _exclude_objects(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, include_uninfected=False)
628
+
629
+ # Update data with the new masks
630
+ if settings['cell_mask_dim'] is not None:
631
+ data[:, :, settings['cell_mask_dim']] = cell_mask.astype(data_type)
632
+ if settings['nucleus_mask_dim'] is not None:
633
+ data[:, :, settings['nucleus_mask_dim']] = nucleus_mask.astype(data_type)
634
+ if settings['pathogen_mask_dim'] is not None:
635
+ data[:, :, settings['pathogen_mask_dim']] = pathogen_mask.astype(data_type)
636
+ if settings['cytoplasm']:
637
+ data = np.concatenate((data, cytoplasm_mask[:, :, np.newaxis]), axis=2)
638
+
639
+ if settings['plot_filtration']:
640
+ _plot_cropped_arrays(data)
641
+
642
+ if settings['save_measurements']:
643
+
644
+ cell_df, nucleus_df, pathogen_df, cytoplasm_df = _morphological_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, settings)
645
+
646
+ cell_intensity_df, nucleus_intensity_df, pathogen_intensity_df, cytoplasm_intensity_df = _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, channel_arrays, settings, sizes=[1, 2, 3, 4, 5], periphery=True, outside=True)
647
+ if settings['cell_mask_dim'] is not None:
648
+ cell_merged_df = _merge_and_save_to_database(cell_df, cell_intensity_df, 'cell', source_folder, file_name, settings['experiment'], settings['timelapse'])
649
+
650
+ if settings['nucleus_mask_dim'] is not None:
651
+ nucleus_merged_df = _merge_and_save_to_database(nucleus_df, nucleus_intensity_df, 'nucleus', source_folder, file_name, settings['experiment'], settings['timelapse'])
652
+
653
+ if settings['pathogen_mask_dim'] is not None:
654
+ pathogen_merged_df = _merge_and_save_to_database(pathogen_df, pathogen_intensity_df, 'pathogen', source_folder, file_name, settings['experiment'], settings['timelapse'])
655
+
656
+ if settings['cytoplasm']:
657
+ cytoplasm_merged_df = _merge_and_save_to_database(cytoplasm_df, cytoplasm_intensity_df, 'cytoplasm', source_folder, file_name, settings['experiment'], settings['timelapse'])
658
+
659
+
660
+ if settings['save_png'] or settings['save_arrays'] or settings['plot']:
661
+
662
+ if isinstance(settings['dialate_pngs'], bool):
663
+ dialate_pngs = [settings['dialate_pngs'], settings['dialate_pngs'], settings['dialate_pngs']]
664
+ if isinstance(settings['dialate_pngs'], list):
665
+ dialate_pngs = settings['dialate_pngs']
666
+
667
+ if isinstance(settings['dialate_png_ratios'], float):
668
+ dialate_png_ratios = [settings['dialate_png_ratios'], settings['dialate_png_ratios'], settings['dialate_png_ratios']]
669
+
670
+ if isinstance(settings['dialate_png_ratios'], list):
671
+ dialate_png_ratios = settings['dialate_png_ratios']
672
+
673
+ if isinstance(settings['crop_mode'], str):
674
+ crop_mode = [settings['crop_mode']]
675
+ if isinstance(settings['crop_mode'], list):
676
+ crop_ls = settings['crop_mode']
677
+ size_ls = settings['png_size']
678
+ for crop_idx, crop_mode in enumerate(crop_ls):
679
+ print(crop_idx, crop_mode)
680
+ width, height = size_ls[crop_idx]
681
+ if crop_mode == 'cell':
682
+ crop_mask = cell_mask.copy()
683
+ dialate_png = dialate_pngs[crop_idx]
684
+ dialate_png_ratio = dialate_png_ratios[crop_idx]
685
+ elif crop_mode == 'nucleus':
686
+ crop_mask = nucleus_mask.copy()
687
+ dialate_png = dialate_pngs[crop_idx]
688
+ dialate_png_ratio = dialate_png_ratios[crop_idx]
689
+ elif crop_mode == 'pathogen':
690
+ crop_mask = pathogen_mask.copy()
691
+ dialate_png = dialate_pngs[crop_idx]
692
+ dialate_png_ratio = dialate_png_ratios[crop_idx]
693
+ elif crop_mode == 'cytoplasm':
694
+ crop_mask = cytoplasm_mask.copy()
695
+ dialate_png = False
696
+ else:
697
+ print(f'Value error: Posseble values for crop_mode are: cell, nucleus, pathogen, cytoplasm')
698
+
699
+ objects_in_image = np.unique(crop_mask)
700
+ objects_in_image = objects_in_image[objects_in_image != 0]
701
+ img_paths = []
702
+
703
+ for _id in objects_in_image:
704
+
705
+ region = (crop_mask == _id) # This creates a boolean mask for the region of interest
706
+
707
+ # Use the boolean mask to filter the cell_mask and then find unique IDs
708
+ region_cell_ids = np.atleast_1d(np.unique(cell_mask[region]))
709
+ region_nucleus_ids = np.atleast_1d(np.unique(nucleus_mask[region]))
710
+ region_pathogen_ids = np.atleast_1d(np.unique(pathogen_mask[region]))
711
+
712
+ if settings['use_bounding_box']:
713
+ region = _find_bounding_box(crop_mask, _id, buffer=10)
714
+
715
+ img_name, fldr, table_name = _generate_names(file_name=file_name, cell_id = region_cell_ids, cell_nucleus_ids=region_nucleus_ids, cell_pathogen_ids=region_pathogen_ids, source_folder=source_folder, crop_mode=crop_mode)
716
+
717
+ if dialate_png:
718
+ region_area = np.sum(region)
719
+ approximate_diameter = np.sqrt(region_area)
720
+ dialate_png_px = int(approximate_diameter * dialate_png_ratio)
721
+ struct = generate_binary_structure(2, 2)
722
+ region = binary_dilation(region, structure=struct, iterations=dialate_png_px)
723
+
724
+ if settings['save_png']:
725
+ fldr_type = f"{crop_mode}_png/"
726
+ png_folder = os.path.join(fldr,fldr_type)
727
+
728
+ img_path = os.path.join(png_folder, img_name)
729
+
730
+ png_channels = data[:, :, settings['png_dims']].astype(data_type)
731
+
732
+ if settings['normalize_by'] == 'fov':
733
+ percentiles_list = _get_percentiles(png_channels, settings['normalize_percentiles'][0],q2=settings['normalize_percentiles'][1])
734
+
735
+ png_channels = _crop_center(png_channels, region, new_width=width, new_height=height)
736
+
737
+ if isinstance(settings['normalize'], list):
738
+ if settings['normalize_by'] == 'png':
739
+ png_channels = normalize_to_dtype(png_channels, q1=settings['normalize'][0],q2=settings['normalize'][1])
740
+
741
+ if settings['normalize_by'] == 'fov':
742
+ png_channels = normalize_to_dtype(png_channels, q1=settings['normalize'][0],q2=settings['normalize'][1], percentiles=percentiles_list)
743
+
744
+ os.makedirs(png_folder, exist_ok=True)
745
+
746
+ if png_channels.shape[2] == 2:
747
+ dummy_channel = np.zeros_like(png_channels[:,:,0]) # Create a 2D zero array with same shape as one channel
748
+ png_channels = np.dstack((png_channels, dummy_channel))
749
+ cv2.imwrite(img_path, png_channels)
750
+ else:
751
+ cv2.imwrite(img_path, png_channels)
752
+
753
+ img_paths.append(img_path)
754
+
755
+ if len(img_paths) == len(objects_in_image):
756
+
757
+ png_df = pd.DataFrame(img_paths, columns=['png_path'])
758
+
759
+ png_df['file_name'] = png_df['png_path'].apply(lambda x: os.path.basename(x))
760
+
761
+ parts = png_df['file_name'].apply(lambda x: pd.Series(_map_wells_png(x, timelapse=settings['timelapse'])))
762
+
763
+ columns = ['plate', 'row', 'col', 'field']
764
+
765
+ if settings['timelapse']:
766
+ columns = columns + ['time_id']
767
+
768
+ columns = columns + ['prcfo']
769
+
770
+ if crop_mode == 'cell':
771
+ columns = columns + ['cell_id']
772
+
773
+ if crop_mode == 'nucleus':
774
+ columns = columns + ['nucleus_id']
775
+
776
+ if crop_mode == 'pathogen':
777
+ columns = columns + ['pathogen_id']
778
+
779
+ if crop_mode == 'cytoplasm':
780
+ columns = columns + ['cytoplasm_id']
781
+
782
+ png_df[columns] = parts
783
+
784
+ try:
785
+ conn = sqlite3.connect(f'{source_folder}/measurements/measurements.db', timeout=5)
786
+ png_df.to_sql('png_list', conn, if_exists='append', index=False)
787
+ conn.commit()
788
+ except sqlite3.OperationalError as e:
789
+ print(f"SQLite error: {e}", flush=True)
790
+
791
+ if settings['plot']:
792
+ _plot_cropped_arrays(png_channels)
793
+
794
+ if settings['save_arrays']:
795
+ row_idx, col_idx = np.where(region)
796
+ region_array = data[row_idx.min():row_idx.max()+1, col_idx.min():col_idx.max()+1, :]
797
+ array_folder = f"{fldr}/region_array/"
798
+ os.makedirs(array_folder, exist_ok=True)
799
+ np.save(os.path.join(array_folder, img_name), region_array)
800
+ if settings['plot']:
801
+ _plot_cropped_arrays(region_array)
802
+
803
+ if not settings['save_arrays'] and not settings['save_png'] and settings['plot']:
804
+ row_idx, col_idx = np.where(region)
805
+ region_array = data[row_idx.min():row_idx.max()+1, col_idx.min():col_idx.max()+1, :]
806
+ _plot_cropped_arrays(region_array)
807
+
808
+ cells = np.unique(cell_mask)
809
+ except Exception as e:
810
+ print('main',e)
811
+ cells = 0
812
+ traceback.print_exc()
813
+
814
+ end = time.time()
815
+ duration = end-start
816
+ time_ls.append(duration)
817
+ average_time = np.mean(time_ls) if len(time_ls) > 0 else 0
818
+ return average_time, cells
819
+
820
+ @log_function_call
821
+ def measure_crop(settings, annotation_settings, advanced_settings):
822
+ """
823
+ Measure the crop of an image based on the provided settings.
824
+
825
+ Args:
826
+ settings (dict): The settings for measuring the crop.
827
+ annotation_settings (dict): The annotation settings.
828
+ advanced_settings (dict): The advanced settings.
829
+
830
+ Returns:
831
+ None
832
+ """
833
+
834
+ from .io import _save_settings_to_db
835
+ from .timelapse import _timelapse_masks_to_gif, _scmovie
836
+ from .plot import _save_scimg_plot
837
+ from .utils import _list_endpoint_subdirectories, _generate_representative_images
838
+
839
+ settings = {**settings, **annotation_settings, **advanced_settings}
840
+
841
+ dirname = os.path.dirname(settings['input_folder'])
842
+ settings_df = pd.DataFrame(list(settings.items()), columns=['Key', 'Value'])
843
+ settings_csv = os.path.join(dirname,'settings','measure_crop_settings.csv')
844
+ os.makedirs(os.path.join(dirname,'settings'), exist_ok=True)
845
+ settings_df.to_csv(settings_csv, index=False)
846
+
847
+ if settings['timelapse_objects'] == 'nucleus':
848
+ if not settings['cell_mask_dim'] is None:
849
+ tlo = settings['timelapse_objects']
850
+ print(f'timelapse object:{tlo}, cells will be relabeled to nucleus labels to track cells.')
851
+
852
+ #general settings
853
+ settings['merge_edge_pathogen_cells'] = True
854
+ settings['radial_dist'] = True
855
+ settings['calculate_correlation'] = True
856
+ settings['manders_thresholds'] = [15,85,95]
857
+ settings['homogeneity'] = True
858
+ settings['homogeneity_distances'] = [8,16,32]
859
+ settings['save_arrays'] = False
860
+
861
+ if settings['cell_mask_dim'] is None:
862
+ settings['include_uninfected'] = True
863
+ if settings['pathogen_mask_dim'] is None:
864
+ settings['include_uninfected'] = True
865
+ if settings['cell_mask_dim'] is not None and settings['pathogen_min_size'] is not None:
866
+ settings['cytoplasm'] = True
867
+ elif settings['cell_mask_dim'] is not None and settings['nucleus_min_size'] is not None:
868
+ settings['cytoplasm'] = True
869
+ else:
870
+ settings['cytoplasm'] = False
871
+
872
+ settings['center_crop'] = True
873
+
874
+ int_setting_keys = ['cell_mask_dim', 'nucleus_mask_dim', 'pathogen_mask_dim', 'cell_min_size', 'nucleus_min_size', 'pathogen_min_size', 'cytoplasm_min_size']
875
+
876
+ if isinstance(settings['normalize'], bool) and settings['normalize']:
877
+ print(f'WARNING: to notmalize single object pngs set normalize to a list of 2 integers, e.g. [1,99] (lower and upper percentiles)')
878
+ return
879
+
880
+ if settings['normalize_by'] not in ['png', 'fov']:
881
+ print("Warning: normalize_by should be either 'png' to notmalize each png to its own percentiles or 'fov' to normalize each png to the fov percentiles ")
882
+ return
883
+
884
+ if not all(isinstance(settings[key], int) or settings[key] is None for key in int_setting_keys):
885
+ print(f"WARNING: {int_setting_keys} must all be integers")
886
+ return
887
+
888
+ if not isinstance(settings['channels'], list):
889
+ print(f"WARNING: channels should be a list of integers representing channels e.g. [0,1,2,3]")
890
+ return
891
+
892
+ if not isinstance(settings['crop_mode'], list):
893
+ print(f"WARNING: crop_mode should be a list with at least one element e.g. ['cell'] or ['cell','nucleus'] or [None]")
894
+ return
895
+
896
+ _save_settings_to_db(settings)
897
+
898
+ files = [f for f in os.listdir(settings['input_folder']) if f.endswith('.npy')]
899
+ max_workers = settings['max_workers'] or mp.cpu_count()-4
900
+ print(f'using {max_workers} cpu cores')
901
+
902
+ with mp.Manager() as manager:
903
+ time_ls = manager.list()
904
+ with mp.Pool(max_workers) as pool:
905
+ result = pool.starmap_async(_measure_crop_core, [(index, time_ls, file, settings) for index, file in enumerate(files)])
906
+
907
+ # Track progress in the main process
908
+ while not result.ready(): # Run the loop until all tasks have finished
909
+ time.sleep(1) # Wait for a short amount of time to avoid excessive printing
910
+ files_processed = len(time_ls)
911
+ files_to_process = len(files)
912
+ average_time = np.mean(time_ls) if len(time_ls) > 0 else 0
913
+ time_left = (((files_to_process-files_processed)*average_time)/max_workers)/60
914
+ print(f'Progress: {files_processed}/{files_to_process} Time/img {average_time:.3f}sec, Time Remaining {time_left:.3f} min.', end='\r', flush=True)
915
+ result.get()
916
+
917
+ #if settings['save_png']:
918
+ # img_fldr = os.path.join(os.path.dirname(settings['input_folder']), 'data')
919
+ # sc_img_fldrs = _list_endpoint_subdirectories(img_fldr)
920
+ # for well_src in sc_img_fldrs:
921
+ # if len(os.listdir(well_src)) < 16:
922
+ # nr_imgs = len(os.listdir(well_src))
923
+ # standardize = False
924
+ # else:
925
+ # nr_imgs = 16
926
+ # standardize = True
927
+ # try:
928
+ # _save_scimg_plot(src=well_src, nr_imgs=nr_imgs, channel_indices=settings['png_dims'], um_per_pixel=0.1, scale_bar_length_um=10, standardize=standardize, fontsize=12, show_filename=True, channel_names=['red','green','blue'], dpi=300, plot=False)
929
+ # except Exception as e: # Consider catching a more specific exception if possible
930
+ # print(f"Unable to generate figure for folder {well_src}: {e}", flush=True)
931
+
932
+ if settings['save_png']:
933
+ img_fldr = os.path.join(os.path.dirname(settings['input_folder']), 'data')
934
+ sc_img_fldrs = _list_endpoint_subdirectories(img_fldr)
935
+
936
+ for i, well_src in enumerate(sc_img_fldrs):
937
+ if len(os.listdir(well_src)) < 16:
938
+ nr_imgs = len(os.listdir(well_src))
939
+ standardize = False
940
+ else:
941
+ nr_imgs = 16
942
+ standardize = True
943
+ try:
944
+ all_folders = len(sc_img_fldrs)
945
+ _save_scimg_plot(src=well_src, nr_imgs=nr_imgs, channel_indices=settings['png_dims'], um_per_pixel=0.1, scale_bar_length_um=10, standardize=standardize, fontsize=12, show_filename=True, channel_names=['red','green','blue'], dpi=300, plot=False, i=i, all_folders=all_folders)
946
+
947
+ except Exception as e:
948
+ print(f"Unable to generate figure for folder {well_src}: {e}", end='\r', flush=True)
949
+ #traceback.print_exc()
950
+
951
+ if settings['save_measurements']:
952
+ if settings['representative_images']:
953
+ db_path = os.path.join(os.path.dirname(settings['input_folder']), 'measurements', 'measurements.db')
954
+ channel_indices = settings['png_dims']
955
+ channel_indices = [min(value, 2) for value in channel_indices]
956
+ _generate_representative_images(db_path,
957
+ cells=settings['cells'],
958
+ cell_loc=settings['cell_loc'],
959
+ pathogens=settings['pathogens'],
960
+ pathogen_loc=settings['pathogen_loc'],
961
+ treatments=settings['treatments'],
962
+ treatment_loc=settings['treatment_loc'],
963
+ channel_of_interest=settings['channel_of_interest'],
964
+ compartments = settings['compartments'],
965
+ measurement = settings['measurement'],
966
+ nr_imgs=settings['nr_imgs'],
967
+ channel_indices=channel_indices,
968
+ um_per_pixel=settings['um_per_pixel'],
969
+ scale_bar_length_um=10,
970
+ plot=False,
971
+ fontsize=12,
972
+ show_filename=True,
973
+ channel_names=None)
974
+
975
+ if settings['timelapse']:
976
+ if settings['timelapse_objects'] == 'nucleus':
977
+ folder_path = settings['input_folder']
978
+ mask_channels = [settings['nucleus_mask_dim'], settings['pathogen_mask_dim'],settings['cell_mask_dim']]
979
+ object_types = ['nucleus','pathogen','cell']
980
+ _timelapse_masks_to_gif(folder_path, mask_channels, object_types)
981
+
982
+ if settings['save_png']:
983
+ img_fldr = os.path.join(os.path.dirname(settings['input_folder']), 'data')
984
+ sc_img_fldrs = _list_endpoint_subdirectories(img_fldr)
985
+ _scmovie(sc_img_fldrs)
986
+
987
+
988
+ def generate_cellpose_train_set(folders, dst, min_objects=5):
989
+ os.makedirs(dst, exist_ok=True)
990
+ os.makedirs(os.path.join(dst,'masks'), exist_ok=True)
991
+ os.makedirs(os.path.join(dst,'imgs'), exist_ok=True)
992
+
993
+ for folder in folders:
994
+ mask_folder = os.path.join(folder, 'masks')
995
+ experiment_id = os.path.basename(folder)
996
+ for filename in os.listdir(mask_folder): # List the contents of the directory
997
+ path = os.path.join(mask_folder, filename)
998
+ img_path = os.path.join(folder, filename)
999
+ newname = experiment_id + '_' + filename
1000
+ new_mask = os.path.join(dst, 'masks', newname)
1001
+ new_img = os.path.join(dst, 'imgs', newname)
1002
+
1003
+ mask = cv2.imread(path, cv2.IMREAD_UNCHANGED)
1004
+ if mask is None:
1005
+ print(f"Error reading {path}, skipping.")
1006
+ continue
1007
+
1008
+ nr_of_objects = len(np.unique(mask)) - 1 # Assuming 0 is background
1009
+ if nr_of_objects >= min_objects: # Use >= to include min_objects
1010
+ try:
1011
+ shutil.copy(path, new_mask)
1012
+ shutil.copy(img_path, new_img)
1013
+ except Exception as e:
1014
+ print(f"Error copying {path} to {new_mask}: {e}")