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/__init__.py +37 -0
- spacr/__main__.py +15 -0
- spacr/annotate_app.py +495 -0
- spacr/cli.py +203 -0
- spacr/core.py +2250 -0
- spacr/gui_mask_app.py +247 -0
- spacr/gui_measure_app.py +214 -0
- spacr/gui_utils.py +488 -0
- spacr/io.py +2271 -0
- spacr/logger.py +20 -0
- spacr/mask_app.py +818 -0
- spacr/measure.py +1014 -0
- spacr/old_code.py +104 -0
- spacr/plot.py +1273 -0
- spacr/sim.py +1187 -0
- spacr/timelapse.py +576 -0
- spacr/train.py +494 -0
- spacr/umap.py +689 -0
- spacr/utils.py +2726 -0
- spacr/version.py +19 -0
- spacr-0.0.1.dist-info/LICENSE +21 -0
- spacr-0.0.1.dist-info/METADATA +64 -0
- spacr-0.0.1.dist-info/RECORD +26 -0
- spacr-0.0.1.dist-info/WHEEL +5 -0
- spacr-0.0.1.dist-info/entry_points.txt +5 -0
- spacr-0.0.1.dist-info/top_level.txt +1 -0
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}")
|