macsima2mc 1.2.3__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.
- macsima2mc/CLI.py +84 -0
- macsima2mc/__init__.py +0 -0
- macsima2mc/exceptions.py +92 -0
- macsima2mc/illumination_corr.py +64 -0
- macsima2mc/macsima2mc.py +50 -0
- macsima2mc/mc_tools.py +110 -0
- macsima2mc/ome_schema.py +179 -0
- macsima2mc/ome_writer.py +30 -0
- macsima2mc/qc.py +77 -0
- macsima2mc/templates.py +65 -0
- macsima2mc/tools.py +406 -0
- macsima2mc-1.2.3.dist-info/LICENSE +28 -0
- macsima2mc-1.2.3.dist-info/METADATA +64 -0
- macsima2mc-1.2.3.dist-info/RECORD +17 -0
- macsima2mc-1.2.3.dist-info/WHEEL +5 -0
- macsima2mc-1.2.3.dist-info/entry_points.txt +2 -0
- macsima2mc-1.2.3.dist-info/top_level.txt +1 -0
macsima2mc/CLI.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
#---CLI-BLOCK---#
|
|
5
|
+
def get_args():
|
|
6
|
+
"""
|
|
7
|
+
This function parses the command line arguments and returns them as a namespace object.
|
|
8
|
+
|
|
9
|
+
returns: namespace object with the arguments.
|
|
10
|
+
"""
|
|
11
|
+
parser=argparse.ArgumentParser()
|
|
12
|
+
|
|
13
|
+
#Mandatory arguments
|
|
14
|
+
parser.add_argument('-i',
|
|
15
|
+
'--input',
|
|
16
|
+
required=True,
|
|
17
|
+
type=pathlib.Path,
|
|
18
|
+
help='Path to the cycle folder'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
parser.add_argument('-o',
|
|
22
|
+
'--output',
|
|
23
|
+
required=True,
|
|
24
|
+
type=pathlib.Path,
|
|
25
|
+
help='Path where the stacks will be saved. If directory does not exist it will be created.'
|
|
26
|
+
)
|
|
27
|
+
#Optional arguments
|
|
28
|
+
parser.add_argument('-rm',
|
|
29
|
+
'--reference_marker',
|
|
30
|
+
default='DAPI',
|
|
31
|
+
help='string specifying the name of the reference marker'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
parser.add_argument('-osd',
|
|
35
|
+
'--output_subdir',
|
|
36
|
+
default='raw',
|
|
37
|
+
help='string specifying the name of the subfolder in which the staged images will be saved'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
parser.add_argument('-ic',
|
|
41
|
+
'--illumination_correction',
|
|
42
|
+
action='store_true',
|
|
43
|
+
help='Applies illumination correction to all tiles, the illumination profiles are created with basicpy'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
parser.add_argument('-he',
|
|
48
|
+
'--hi_exposure_only',
|
|
49
|
+
action='store_true',
|
|
50
|
+
help='Activate this flag to extract only the set of images with the highest exposure time.'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
parser.add_argument('-rr',
|
|
54
|
+
'--remove_reference_marker',
|
|
55
|
+
action='store_true',
|
|
56
|
+
help='It will mark the removal of the reference markers in the markers.csv file except for the first cycle. Use this when you \
|
|
57
|
+
dont want to keep e.g. the DAPI images of the other cycles.'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument('-qc',
|
|
61
|
+
'--qc_metrics',
|
|
62
|
+
action='store_true',
|
|
63
|
+
help='measure features of contrast, intensity and sharpness of each image'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
parser.add_argument('-oqc',
|
|
67
|
+
'--only_qc_file',
|
|
68
|
+
action='store_true',
|
|
69
|
+
help='skips the stacking of the tiles and only calculates the qc table. Still flag -wt is required to write the table. '
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
parser.add_argument('-wt',
|
|
73
|
+
'--write_table',
|
|
74
|
+
action='store_true',
|
|
75
|
+
help='writes a table in --output/cycle_info. Content of table is acquisition parameters, metadata and, if enabled, qc metrics of each tile'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
args=parser.parse_args()
|
|
79
|
+
|
|
80
|
+
return args
|
|
81
|
+
#---END_CLI-BLOCK---#
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
macsima2mc/__init__.py
ADDED
|
File without changes
|
macsima2mc/exceptions.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def any_ref(mf_tuple,
|
|
6
|
+
ref_marker='DAPI'):
|
|
7
|
+
"""
|
|
8
|
+
This function checks if the reference marker is present in the mf_tuple.
|
|
9
|
+
Args:
|
|
10
|
+
mf_tuple (tuple): tuple with the markers and filters
|
|
11
|
+
ref_marker (str): reference marker used for registration
|
|
12
|
+
Returns:
|
|
13
|
+
bool: True if the reference marker is present in the mf_tuple, False otherwise.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
exist_ref = False
|
|
17
|
+
for m in mf_tuple:
|
|
18
|
+
if m[0] == ref_marker:
|
|
19
|
+
exist_ref = True
|
|
20
|
+
break
|
|
21
|
+
return exist_ref
|
|
22
|
+
|
|
23
|
+
def miss_channel(chann_index,chans_meta,conformed_markers):
|
|
24
|
+
|
|
25
|
+
if chans_meta[ chann_index ].size:
|
|
26
|
+
pass
|
|
27
|
+
else:
|
|
28
|
+
|
|
29
|
+
ref_index=np.argwhere([meta.size > 0 for meta in chans_meta ]).flatten()[0]
|
|
30
|
+
chann_meta=chans_meta[ ref_index ]
|
|
31
|
+
edit_feats={'marker':conformed_markers[ chann_index ][0],
|
|
32
|
+
'filter':conformed_markers[ chann_index ][1]}
|
|
33
|
+
#'exposure?':}
|
|
34
|
+
#how are conformed markers formed?
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def at_roi(grouped,dimensions,ref_marker):
|
|
40
|
+
#for dimensions is expected that the last element of the list is the exposure_level
|
|
41
|
+
rows=[]
|
|
42
|
+
for index,frame in grouped:
|
|
43
|
+
marker_filter_map = list( frame.groupby(["marker","filter"]).indices.keys() )
|
|
44
|
+
ref_exists=any_ref(marker_filter_map,ref_marker)
|
|
45
|
+
subst_exp_level=index[-1]
|
|
46
|
+
rows.append( list(index) + [ ref_exists, subst_exp_level ] )
|
|
47
|
+
|
|
48
|
+
df=pd.DataFrame(rows,columns=dimensions+['ref_marker','aux_exp_level'])
|
|
49
|
+
|
|
50
|
+
frames=[]
|
|
51
|
+
for index,frame in df.groupby(dimensions[:-1]):
|
|
52
|
+
substitute=frame.loc[ frame['ref_marker']==True,'exposure_level'].max()
|
|
53
|
+
frame['aux_exp_level']=frame['aux_exp_level'].where(frame['ref_marker'],substitute)
|
|
54
|
+
frames.append(frame)
|
|
55
|
+
|
|
56
|
+
return pd.concat(frames)
|
|
57
|
+
|
|
58
|
+
def at_tile(group,marker_filter_map):
|
|
59
|
+
rows=[]
|
|
60
|
+
for tile_id,frame in group.groupby(['tile']):
|
|
61
|
+
aux_array = [ frame.loc[ (frame['marker']==marker) & (frame['filter']==filt)].size
|
|
62
|
+
for marker, filt in marker_filter_map
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
aux_array =np.array(aux_array)==0
|
|
66
|
+
missing_channel=np.any(aux_array)
|
|
67
|
+
|
|
68
|
+
if missing_channel:
|
|
69
|
+
channel_indices=np.argwhere(aux_array).flatten()
|
|
70
|
+
#Each missing (marker,filter) pair will be separated by a colon ":", i.e. marker1,filter1:marker2,filter2
|
|
71
|
+
channel= ':'.join( [ ','.join(marker_filter_map[i]) for i in channel_indices] )
|
|
72
|
+
else:
|
|
73
|
+
channel=''
|
|
74
|
+
|
|
75
|
+
qc_data=[tile_id[0],missing_channel,channel,tile_id[0]]
|
|
76
|
+
cols=['tile','missing_ch','channels','aux_tile']
|
|
77
|
+
rows.append( qc_data )
|
|
78
|
+
|
|
79
|
+
df=pd.DataFrame(rows,columns=cols)
|
|
80
|
+
auxiliary_tile_index=df.loc[df['missing_ch']==False ][['tile']].values[0]
|
|
81
|
+
df.loc[ df['missing_ch']==True, ['aux_tile']]=auxiliary_tile_index
|
|
82
|
+
|
|
83
|
+
return df
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from basicpy import BaSiC
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
def indices_per_channel(total_imgs, no_of_channels):
|
|
5
|
+
"""
|
|
6
|
+
This function creates a list of lists with the indices of the images in the stack per channel.
|
|
7
|
+
Args:
|
|
8
|
+
total_imgs (int): total number of images in the stack.
|
|
9
|
+
no_of_channels (int): number of channels.
|
|
10
|
+
Returns:
|
|
11
|
+
list: list of lists with the indices of the images in the stack per channel.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
#total_imgs in the stack
|
|
15
|
+
img_indices = [list(range(ch,total_imgs,no_of_channels)) for ch in range(0, no_of_channels)]
|
|
16
|
+
return img_indices
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract_channel_imgs (stack,indices):
|
|
20
|
+
"""
|
|
21
|
+
This function extracts the images in the stack per channel.
|
|
22
|
+
Args:
|
|
23
|
+
stack (np.array): stack of images.
|
|
24
|
+
indices (list): list of indices of the images in the stack per channel.
|
|
25
|
+
Returns:
|
|
26
|
+
np.array: stack of images per channel.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
return stack[indices,:,:]
|
|
30
|
+
|
|
31
|
+
def filter_out_blanks(channel_tiles):
|
|
32
|
+
|
|
33
|
+
signal_indices=[index for index in range(0,channel_tiles.shape[0]) if np.sum(channel_tiles[index,:,:])>0 ]
|
|
34
|
+
filtered_tiles=channel_tiles[signal_indices,:,:]
|
|
35
|
+
return filtered_tiles
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def apply_corr(uncorr_stack,no_of_channels):
|
|
40
|
+
"""
|
|
41
|
+
This function applies the BaSiC algorithm to correct the illumination in the images.
|
|
42
|
+
Args:
|
|
43
|
+
uncorr_stack (np.array): stack of uncorrected images.
|
|
44
|
+
no_of_channels (int): number of channels.
|
|
45
|
+
Returns:
|
|
46
|
+
np.array: stack of corrected images.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
corr_stack = np.zeros( uncorr_stack.shape,dtype=uncorr_stack.dtype )
|
|
50
|
+
total_imgs = uncorr_stack.shape[0]
|
|
51
|
+
indices = indices_per_channel(total_imgs, no_of_channels)
|
|
52
|
+
basic = BaSiC(get_darkfield=False, smoothness_flatfield=1.0, fitting_mode="approximate", sort_intensity=True)
|
|
53
|
+
|
|
54
|
+
for ind_list in indices:
|
|
55
|
+
uncorr_imgs = extract_channel_imgs(uncorr_stack, ind_list)
|
|
56
|
+
imgs_for_ffp=filter_out_blanks(uncorr_imgs)
|
|
57
|
+
#basic.fit(uncorr_imgs)
|
|
58
|
+
basic.fit(imgs_for_ffp)
|
|
59
|
+
ffp = basic.flatfield
|
|
60
|
+
corr_stack[ind_list,:,:] = np.uint16(np.clip(uncorr_imgs.astype(float) / ffp, 0, 65535))
|
|
61
|
+
|
|
62
|
+
return corr_stack
|
|
63
|
+
|
|
64
|
+
|
macsima2mc/macsima2mc.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import tools
|
|
2
|
+
from templates import macsima_pattern
|
|
3
|
+
import CLI
|
|
4
|
+
import mc_tools
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
# Get arguments
|
|
8
|
+
args = CLI.get_args()
|
|
9
|
+
|
|
10
|
+
# Assign arguments to variables
|
|
11
|
+
input = args.input
|
|
12
|
+
output = args.output
|
|
13
|
+
ref = args.reference_marker
|
|
14
|
+
basicpy_corr = args.illumination_correction
|
|
15
|
+
out_folder_name = args.output_subdir
|
|
16
|
+
|
|
17
|
+
# Extract acquisition info of the cycle from file name,e.g. rack,well,markers,filters, etc.
|
|
18
|
+
cycle_info = tools.cycle_info(input, macsima_pattern(version=2), ref_marker= ref)
|
|
19
|
+
|
|
20
|
+
# Extract and append ome metadata info contained in each file
|
|
21
|
+
cycle_info = tools.append_metadata( cycle_info )
|
|
22
|
+
|
|
23
|
+
# Create stack
|
|
24
|
+
cycle_number=int(cycle_info['cycle'].unique()[0])
|
|
25
|
+
if args.only_qc_file:
|
|
26
|
+
pass
|
|
27
|
+
else:
|
|
28
|
+
output_dirs = tools.create_stack(cycle_info,
|
|
29
|
+
output,
|
|
30
|
+
ref_marker=ref,
|
|
31
|
+
hi_exp=args.hi_exposure_only,
|
|
32
|
+
ill_corr=basicpy_corr,
|
|
33
|
+
out_folder=out_folder_name)
|
|
34
|
+
# Save markers file in each output directory
|
|
35
|
+
for path in output_dirs:
|
|
36
|
+
mc_tools.write_markers_file(path,args.remove_reference_marker)
|
|
37
|
+
|
|
38
|
+
# Calculate and append ome metadata info contained in each file
|
|
39
|
+
if (args.qc_metrics or args.only_qc_file) :
|
|
40
|
+
import qc
|
|
41
|
+
cycle_info=qc.append_qc(cycle_info)
|
|
42
|
+
|
|
43
|
+
if args.write_table:
|
|
44
|
+
qc_output_dir=output / "cycle_info"
|
|
45
|
+
qc_output_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
cycle_info.to_csv( qc_output_dir / 'cycle_{c}.csv'.format( c=f'{ cycle_number:03d}' ), index=False )
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == '__main__':
|
|
50
|
+
main()
|
macsima2mc/mc_tools.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from tools import extract_values
|
|
3
|
+
import re
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ome_types import from_tiff
|
|
6
|
+
from pandas import DataFrame
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def flatten_list(lol):
|
|
11
|
+
"""
|
|
12
|
+
This function flattens a list of lists.
|
|
13
|
+
Args:
|
|
14
|
+
lol (list): list of lists.
|
|
15
|
+
Returns:
|
|
16
|
+
list: flattened list.
|
|
17
|
+
"""
|
|
18
|
+
return [x for xs in lol for x in xs]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def markers_file():
|
|
22
|
+
"""
|
|
23
|
+
This function creates a dictionary with the columns of the markers.csv file.
|
|
24
|
+
Returns:
|
|
25
|
+
dict: dictionary with the columns of the markers.csv file.
|
|
26
|
+
"""
|
|
27
|
+
columns = {'channel_number': [],
|
|
28
|
+
'cycle_number': [],
|
|
29
|
+
'marker_name':[],
|
|
30
|
+
'Filter':[],
|
|
31
|
+
'background':[],
|
|
32
|
+
'exposure':[],
|
|
33
|
+
'remove':[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return columns
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_patterns():
|
|
40
|
+
"""
|
|
41
|
+
Dictionary of tuples where first element indicates the pattern to search and
|
|
42
|
+
second element is a boolean that indicates whether to transform the search output into a value or not
|
|
43
|
+
the keys of this dictionary should be a subset of the keys in the markers_file function.
|
|
44
|
+
Returns:
|
|
45
|
+
dict: dictionary with regular expressions to extract metadata from macsima filenames.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
patterns = {
|
|
49
|
+
'cycle_number': (r"cycle-(.*?)-", True),
|
|
50
|
+
'marker_name' : (r"markers-(.*?)-filters",False),
|
|
51
|
+
'Filter' : (r"-filters-(.*?).ome",False),
|
|
52
|
+
'background' : (r"-src-(.*?)-" ,False)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return patterns
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write_markers_file( data_path, rm_ref_marker,ref_marker='DAPI'):
|
|
59
|
+
"""
|
|
60
|
+
This function writes the markers.csv file.
|
|
61
|
+
Args:
|
|
62
|
+
data_path (Path): path to the folder containing the images.
|
|
63
|
+
rm_ref_marker(Boolean): mark reference markers for removal except for the first one
|
|
64
|
+
ref_marker (str): reference marker.
|
|
65
|
+
Returns:
|
|
66
|
+
dict: dictionary with the columns of the markers.csv file.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
img_paths = list(sorted( data_path.glob('*.tif*')))
|
|
70
|
+
mks_file = markers_file()
|
|
71
|
+
patt = get_patterns()
|
|
72
|
+
|
|
73
|
+
for img in img_paths:
|
|
74
|
+
img_name = [img.stem]
|
|
75
|
+
cycle_no = extract_values(patt['cycle_number'][0], img_name, number_cast=patt['cycle_number'][1])
|
|
76
|
+
background = extract_values(patt['background'][0], img_name, number_cast=patt['background'][1])
|
|
77
|
+
markers = extract_values(patt['marker_name'][0], img_name, number_cast=patt['marker_name'][1])[0].split('__')
|
|
78
|
+
filters = extract_values(patt['Filter'][0], img_name, number_cast=patt['Filter'][1])[0].split('__')
|
|
79
|
+
ome = from_tiff(img)
|
|
80
|
+
|
|
81
|
+
if background[0]=='B':
|
|
82
|
+
remove = len(markers)*['TRUE']
|
|
83
|
+
markers = ['bg_{c}_{m}-{f}'.format( c= f'{cycle_no[0]:03d}' , m=x , f=y ) for x,y in zip(markers,filters) ]
|
|
84
|
+
fmt_background = len(markers)*['']
|
|
85
|
+
else:
|
|
86
|
+
fmt_background = []
|
|
87
|
+
remove = len(markers)*['']
|
|
88
|
+
for x,y in zip(markers,filters):
|
|
89
|
+
if x == ref_marker:
|
|
90
|
+
fmt_background.append('')
|
|
91
|
+
else:
|
|
92
|
+
fmt_background.append(f'bg_{cycle_no[0]:03d}_{x}-{y}')
|
|
93
|
+
|
|
94
|
+
mks_file['cycle_number'].extend(len(markers)*cycle_no)
|
|
95
|
+
mks_file['marker_name'].extend(markers)
|
|
96
|
+
mks_file['Filter'].extend(filters)
|
|
97
|
+
mks_file['background'].extend(fmt_background)
|
|
98
|
+
mks_file['exposure'].extend([ome.images[0].pixels.planes[ch].exposure_time for ch,_ in enumerate(markers)])
|
|
99
|
+
mks_file['remove'].extend(remove)
|
|
100
|
+
|
|
101
|
+
mks_file['channel_number'] = list(range(1, 1 + len(mks_file['marker_name'])))
|
|
102
|
+
mks_file_df = DataFrame(mks_file)
|
|
103
|
+
if rm_ref_marker:
|
|
104
|
+
earliest_cycle=mks_file_df['cycle_number'].min()
|
|
105
|
+
condition=( mks_file_df['marker_name']== ref_marker ) & ( mks_file_df['cycle_number']>earliest_cycle )
|
|
106
|
+
mks_file_df.loc[ condition , ['remove'] ]='TRUE'
|
|
107
|
+
|
|
108
|
+
mks_file_df.to_csv( data_path.parent.absolute() / 'markers.csv' , index=False )
|
|
109
|
+
|
|
110
|
+
return mks_file
|
macsima2mc/ome_schema.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
import ome_types
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
from ome_types.model import OME,Image,Pixels,TiffData,Channel,Plane
|
|
5
|
+
import platform
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def INPUTS(frame, conformed_markers):
|
|
10
|
+
"""
|
|
11
|
+
This function creates a dictionary with the metadata of the tiles.
|
|
12
|
+
Args:
|
|
13
|
+
frame (pd.DataFrame): dataframe containing the metadata of the tiles.
|
|
14
|
+
conformed_markers (list): list of tuples with the name of the markers and their corresponding fluorophore.
|
|
15
|
+
Returns:
|
|
16
|
+
dict: dictionary with the metadata of the tiles.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
#features = frame.columns.values
|
|
20
|
+
#inputs = {column:[] for column in features }
|
|
21
|
+
#metadata = [frame.loc[ (frame['marker']==marker) & (frame['filter']==filt)] for marker, filt in conformed_markers]
|
|
22
|
+
#for meta in metadata:
|
|
23
|
+
# for f in features:
|
|
24
|
+
# inputs[f].append(meta[f].values[0])2
|
|
25
|
+
inputs=frame.to_dict('list')
|
|
26
|
+
|
|
27
|
+
return inputs
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def TIFF_array(no_of_channels, inputs={'offset':0}):
|
|
31
|
+
"""
|
|
32
|
+
This function creates a list of TIFFData objects.
|
|
33
|
+
Args:
|
|
34
|
+
no_of_channels (int): number of channels.
|
|
35
|
+
inputs (dict): dictionary with the metadata of the tiles.
|
|
36
|
+
Returns:
|
|
37
|
+
list: list of TIFFData objects.
|
|
38
|
+
"""
|
|
39
|
+
TIFF = [
|
|
40
|
+
TiffData(
|
|
41
|
+
first_c=ch,
|
|
42
|
+
ifd=n,
|
|
43
|
+
plane_count=1
|
|
44
|
+
)
|
|
45
|
+
for n,ch in enumerate(range(0,no_of_channels), start=inputs['offset'])
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
return TIFF
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def PLANE_array(no_of_channels, inputs):
|
|
52
|
+
"""
|
|
53
|
+
This function creates a list of Plane objects.
|
|
54
|
+
Args:
|
|
55
|
+
no_of_channels (int): number of channels.
|
|
56
|
+
inputs (dict): dictionary with the metadata of the tiles.
|
|
57
|
+
Returns:
|
|
58
|
+
list: list of Plane objects.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
PLANE = [
|
|
62
|
+
Plane(
|
|
63
|
+
the_c=ch,
|
|
64
|
+
the_t=0,
|
|
65
|
+
the_z=0,
|
|
66
|
+
position_x=inputs['position_x'][ch],
|
|
67
|
+
position_y=inputs['position_y'][ch],
|
|
68
|
+
position_z=0,#Z=0 is just a place holder
|
|
69
|
+
exposure_time=inputs['exposure_time'][ch],
|
|
70
|
+
position_x_unit=inputs['position_x_unit'][ch],
|
|
71
|
+
position_y_unit=inputs['position_y_unit'][ch]
|
|
72
|
+
)
|
|
73
|
+
for ch in range(0,no_of_channels)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
return PLANE
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def CHANN_array(no_of_channels, inputs):
|
|
80
|
+
"""
|
|
81
|
+
This function creates a list of Channel objects.
|
|
82
|
+
Args:
|
|
83
|
+
no_of_channels (int): number of channels.
|
|
84
|
+
inputs (dict): dictionary with the metadata of the tiles.
|
|
85
|
+
Returns:
|
|
86
|
+
list: list of Channel objects.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
CHANN = [
|
|
90
|
+
Channel(
|
|
91
|
+
id=f"Channel:{100+int(inputs['tile'][ch])}:{ch}:{inputs['marker'][ch]}", # 'Channel:{y}:{x}:{marker_name}'.format(x=ch,y=100+int( inputs['tile'][ch] ) ,marker_name=inputs['marker'][ch] )
|
|
92
|
+
color=(255,255,255),
|
|
93
|
+
emission_wavelength=inputs['emission_wavelenght'][ch],
|
|
94
|
+
emission_wavelength_unit=inputs['emission_wavelenght_unit'][ch],
|
|
95
|
+
excitation_wavelength=inputs['excitation_wavelenght'][ch],
|
|
96
|
+
excitation_wavelength_unit=inputs['excitation_wavelenght_unit'][ch]
|
|
97
|
+
)
|
|
98
|
+
for ch in range(0,no_of_channels)
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
return CHANN
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def PIXELS_array(chann_block, plane_block, tiff_block, inputs):
|
|
105
|
+
"""
|
|
106
|
+
This function creates a Pixels object.
|
|
107
|
+
Args:
|
|
108
|
+
chann_block (list): list of Channel objects.
|
|
109
|
+
plane_block (list): list of Plane objects.
|
|
110
|
+
tiff_block (list): list of TIFFData objects.
|
|
111
|
+
inputs (dict): dictionary with the metadata of the tiles.
|
|
112
|
+
Returns:
|
|
113
|
+
Pixels: Pixels object.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
PIXELS = Pixels(
|
|
117
|
+
id=f"Pixels:{inputs['tile'][0]}",
|
|
118
|
+
dimension_order='XYCZT',
|
|
119
|
+
size_c=len(chann_block),
|
|
120
|
+
size_t=1,
|
|
121
|
+
size_x=inputs['size_x'][0],
|
|
122
|
+
size_y=inputs['size_y'][0],
|
|
123
|
+
size_z=1,
|
|
124
|
+
type=inputs['type'][0],#bit_depth
|
|
125
|
+
big_endian=False,
|
|
126
|
+
channels=chann_block,
|
|
127
|
+
interleaved=False,
|
|
128
|
+
physical_size_x=inputs['physical_size_x'][0],
|
|
129
|
+
physical_size_x_unit=inputs['physical_size_x_unit'][0],
|
|
130
|
+
physical_size_y=inputs['physical_size_y'][0],
|
|
131
|
+
physical_size_y_unit=inputs['physical_size_y_unit'][0],
|
|
132
|
+
physical_size_z=1.0,
|
|
133
|
+
planes=plane_block,
|
|
134
|
+
significant_bits=inputs['significant_bits'][0],
|
|
135
|
+
tiff_data_blocks=tiff_block
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return PIXELS
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def IMAGE_array(pixels_block, imageID):
|
|
142
|
+
"""
|
|
143
|
+
This function creates an Image object.
|
|
144
|
+
Args:
|
|
145
|
+
pixels_block (Pixels): Pixels object.
|
|
146
|
+
imageID (int): identifier of the image.
|
|
147
|
+
Returns:
|
|
148
|
+
Image: Image object.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
IMAGE = Image(
|
|
152
|
+
id =f'Image:{imageID}',
|
|
153
|
+
pixels=pixels_block
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return IMAGE
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def OME_metadata(image_block):
|
|
160
|
+
"""
|
|
161
|
+
This function creates an OME object.
|
|
162
|
+
Args:
|
|
163
|
+
image_block (list): list of Image objects.
|
|
164
|
+
Returns:
|
|
165
|
+
OME: OME object.
|
|
166
|
+
"""
|
|
167
|
+
ome = OME()
|
|
168
|
+
ome.creator = " ".join([ome_types.__name__,
|
|
169
|
+
ome_types.__version__,
|
|
170
|
+
'/ python version-',
|
|
171
|
+
platform.python_version()
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
ome.images = image_block
|
|
176
|
+
ome.uuid = uuid4().urn
|
|
177
|
+
ome_xml = ome_types.to_xml(ome)
|
|
178
|
+
|
|
179
|
+
return ome, ome_xml
|
macsima2mc/ome_writer.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import ome_schema as schema
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def create_ome(tile_info,
|
|
6
|
+
conformed_markers):
|
|
7
|
+
"""
|
|
8
|
+
This function creates an OME-XML file from a pandas dataframe containing the metadata of the tiles.
|
|
9
|
+
Args:
|
|
10
|
+
tile_info (pd.DataFrame): dataframe containing the metadata of the tiles.
|
|
11
|
+
conformed_markers (list): list of tuples with the name of the markers and their corresponding fluorophore.
|
|
12
|
+
Returns:
|
|
13
|
+
str: OME-XML file.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
grouped_tiles = tile_info.groupby(['tile'])
|
|
17
|
+
no_of_channels = len(conformed_markers)
|
|
18
|
+
tiles_counter = 0
|
|
19
|
+
image = []
|
|
20
|
+
for tileID, frame in grouped_tiles:
|
|
21
|
+
metadata = schema.INPUTS(frame, conformed_markers)
|
|
22
|
+
tiff = schema.TIFF_array(no_of_channels, inputs={'offset': no_of_channels * tiles_counter})
|
|
23
|
+
plane = schema.PLANE_array(no_of_channels, metadata)
|
|
24
|
+
channel = schema.CHANN_array(no_of_channels, metadata)
|
|
25
|
+
pixels = schema.PIXELS_array(channel, plane, tiff, metadata)
|
|
26
|
+
image.append(schema.IMAGE_array (pixels, tiles_counter))
|
|
27
|
+
tiles_counter += 1
|
|
28
|
+
ome, ome_xml = schema.OME_metadata(image)
|
|
29
|
+
|
|
30
|
+
return ome, ome_xml
|
macsima2mc/qc.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from skimage.measure import blur_effect
|
|
3
|
+
from tifffile import imread
|
|
4
|
+
from tools import merge_dicts
|
|
5
|
+
import numpy as np
|
|
6
|
+
from multiprocessing import Pool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def contrast(img):
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
#the dictionary below gives the arguments of the np.roll function to
|
|
13
|
+
#shift the array in the elemental angle specified by the dictionary keys
|
|
14
|
+
|
|
15
|
+
angles={0 : [(-1,0),(1,0)],
|
|
16
|
+
45 : [(-1,1),(1,0)],
|
|
17
|
+
90 : [(0,1),(0,0)],
|
|
18
|
+
135: [(1,1),(1,0)],
|
|
19
|
+
180: [(1,0),(1,0)],
|
|
20
|
+
225: [(-1,1),(0,1)],
|
|
21
|
+
270: [(0,-1),(0,0)],
|
|
22
|
+
315: [(-1,-1),(0,1)]
|
|
23
|
+
}
|
|
24
|
+
diff=np.zeros(img.shape)
|
|
25
|
+
for key,val in angles.items():
|
|
26
|
+
diff+= ( img-np.roll(img,val[0],axis=val[1]) )**2
|
|
27
|
+
|
|
28
|
+
contrast_arr=np.sqrt(diff)
|
|
29
|
+
med=np.median(contrast_arr)
|
|
30
|
+
iqr=np.percentile(contrast_arr, 75)-np.percentile(contrast_arr, 25)
|
|
31
|
+
|
|
32
|
+
return med,iqr
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def calculate_img_qc(img_path):
|
|
38
|
+
"""
|
|
39
|
+
This function extracts the metadata from a tiff file using the ome-xml format.
|
|
40
|
+
Args:
|
|
41
|
+
img_path (Path): full path to the tiff file
|
|
42
|
+
Returns:
|
|
43
|
+
dict: dictionary with qc_metrics for the input file (img_path)
|
|
44
|
+
"""
|
|
45
|
+
img=imread(img_path)
|
|
46
|
+
intensity_med=np.median(img)
|
|
47
|
+
intensity_iqr=np.percentile(img, 75)-np.percentile(img, 25)
|
|
48
|
+
contrast_med,contrast_iqr=contrast(img)
|
|
49
|
+
blur=blur_effect(img)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"intensity_median" : intensity_med ,
|
|
53
|
+
"intensity_iqr" : intensity_iqr ,
|
|
54
|
+
"contrast_median" :contrast_med,
|
|
55
|
+
"contrast_iqr" :contrast_iqr,
|
|
56
|
+
"blur" :blur
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def append_qc(cycle_info_df,append=False):
|
|
62
|
+
|
|
63
|
+
with Pool() as pool:
|
|
64
|
+
qc_feats=pool.map(calculate_img_qc, cycle_info_df['full_path'].values)
|
|
65
|
+
|
|
66
|
+
for key, val in merge_dicts(qc_feats).items():
|
|
67
|
+
cycle_info_df.insert(loc=cycle_info_df.shape[1], column=key, value=val)
|
|
68
|
+
|
|
69
|
+
return cycle_info_df
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
macsima2mc/templates.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
def info_dic(target_pattern):
|
|
4
|
+
"""
|
|
5
|
+
creates a dictionary with keys mirroring the keys in the target_pattern dictionary.
|
|
6
|
+
The value of each key is an empty string.
|
|
7
|
+
Args:
|
|
8
|
+
target_pattern (dict): dictionary taken from the macsima_pattern function.
|
|
9
|
+
Returns:
|
|
10
|
+
dict: template with keys from macsima_pattern plus 2 extra keys, full path of image and image name.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Initialize dictionary
|
|
14
|
+
template = {
|
|
15
|
+
'full_path': '',
|
|
16
|
+
'img_name': ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for key in target_pattern:
|
|
20
|
+
template[key] = ''
|
|
21
|
+
|
|
22
|
+
return template
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def macsima_pattern(version=1):
|
|
26
|
+
"""
|
|
27
|
+
Returns a dictionary with regular expressions to extract metadata from macsima filenames.
|
|
28
|
+
Args:
|
|
29
|
+
version (int): version of the macsima filenames. Default is 1.
|
|
30
|
+
Returns:
|
|
31
|
+
dict: dictionary with regular expressions to extract metadata from macsima filenames.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
if version == 1:
|
|
35
|
+
pattern = {
|
|
36
|
+
"cycle" : r"(.*?)_(.*?)Cycle",
|
|
37
|
+
"source" : r"_(.*?)Cycle",
|
|
38
|
+
"rack" : r"_R-(\d+)",
|
|
39
|
+
"well" : r"_W-(\d+)",
|
|
40
|
+
"roi" : r"_G-(\d+)",
|
|
41
|
+
"tile" : r"_F-(\d+)",
|
|
42
|
+
"exposure_time" : r"_E-(\d+)",
|
|
43
|
+
"marker" : r"Cycle_(.*?)_",
|
|
44
|
+
"filter" : r".*_([^_]*)_\d+bit"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
elif version==2:
|
|
48
|
+
|
|
49
|
+
pattern = {
|
|
50
|
+
"cycle" : r"CYC-(\d+)",
|
|
51
|
+
"source" : r"_ST-(.*?)_",
|
|
52
|
+
"rack" : r"_R-(\d+)",
|
|
53
|
+
"well" : r"_W-(.*?\d+)",
|
|
54
|
+
"roi" : r"_ROI-(\d+)",
|
|
55
|
+
"tile" : r"_F-(\d+)",
|
|
56
|
+
"exposure_time" : r"_EXP-(\d+(?:\.\d+)?)",
|
|
57
|
+
"marker" : r"_A-(.*?)_",
|
|
58
|
+
"filter" : r"_D-(.*?)_"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"version argument should be 1 or 2"
|
|
64
|
+
)
|
|
65
|
+
return pattern
|
macsima2mc/tools.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
from templates import info_dic
|
|
2
|
+
import re
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import tifffile as tifff
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
import numpy as np
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import ome_writer
|
|
9
|
+
import exceptions as expt
|
|
10
|
+
import illumination_corr
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def merge_dicts(list_of_dicts):
|
|
14
|
+
"""
|
|
15
|
+
This function merges a list of dictionaries into a single dictionary where the values are stored in lists.
|
|
16
|
+
Args:
|
|
17
|
+
list_of_dicts (list): list of dictionaries with common keys
|
|
18
|
+
Returns:
|
|
19
|
+
merged_dict (dict): dictionary with the values stored in lists
|
|
20
|
+
"""
|
|
21
|
+
merged_dict = {}
|
|
22
|
+
for d in list_of_dicts:
|
|
23
|
+
for key, value in d.items():
|
|
24
|
+
if key in merged_dict:
|
|
25
|
+
merged_dict[key].append(value)
|
|
26
|
+
else:
|
|
27
|
+
merged_dict[key] = [value]
|
|
28
|
+
return merged_dict
|
|
29
|
+
|
|
30
|
+
def extract_values(target_pattern,
|
|
31
|
+
strings,
|
|
32
|
+
number_cast=True):
|
|
33
|
+
"""
|
|
34
|
+
This function extracts the values from a list of strings using a regular expression pattern.
|
|
35
|
+
Args:
|
|
36
|
+
target_pattern (str): regular expression pattern
|
|
37
|
+
strings (list): list of strings to extract the values from
|
|
38
|
+
number_cast (bool): if True, the extracted values are cast to integers
|
|
39
|
+
Returns:
|
|
40
|
+
list: list of extracted values
|
|
41
|
+
"""
|
|
42
|
+
return [
|
|
43
|
+
(int(m.group(1)) if number_cast else m.group(1))
|
|
44
|
+
if (m := re.search(target_pattern, s))
|
|
45
|
+
else None
|
|
46
|
+
for s in strings
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_metadata(tile_abs_path):
|
|
51
|
+
"""
|
|
52
|
+
This function extracts the metadata from a tiff file using the ome-xml format.
|
|
53
|
+
Args:
|
|
54
|
+
tile_abs_path (Path): full path to the tiff file
|
|
55
|
+
Returns:
|
|
56
|
+
dict: dictionary with the metadata extracted from the tiff file using the ome-xml format.
|
|
57
|
+
"""
|
|
58
|
+
with tifff.TiffFile(tile_abs_path) as tif:
|
|
59
|
+
metadata = tif.ome_metadata
|
|
60
|
+
|
|
61
|
+
ome = BeautifulSoup(metadata, "xml")
|
|
62
|
+
return {
|
|
63
|
+
"position_x": float(ome.StageLabel["X"]),
|
|
64
|
+
"position_y": float(ome.StageLabel["Y"]),
|
|
65
|
+
"position_x_unit": ome.StageLabel["XUnit"],
|
|
66
|
+
"position_y_unit": ome.StageLabel["YUnit"],
|
|
67
|
+
"physical_size_x": float(ome.Pixels["PhysicalSizeX"]),
|
|
68
|
+
"physical_size_x_unit": ome.Pixels["PhysicalSizeXUnit"],
|
|
69
|
+
"physical_size_y": float(ome.Pixels["PhysicalSizeY"]),
|
|
70
|
+
"physical_size_y_unit": ome.Pixels["PhysicalSizeXUnit"],
|
|
71
|
+
"size_x":ome.Pixels["SizeX"],
|
|
72
|
+
"size_y":ome.Pixels["SizeY"],
|
|
73
|
+
"type": ome.Pixels["Type"],#bit_depth
|
|
74
|
+
"significant_bits": int(ome.Pixels["SignificantBits"]),
|
|
75
|
+
"emission_wavelenght":ome.Channel["EmissionWavelength"],
|
|
76
|
+
"excitation_wavelenght":ome.Channel["ExcitationWavelength"],
|
|
77
|
+
"emission_wavelenght_unit":ome.Channel["EmissionWavelengthUnit"],
|
|
78
|
+
"excitation_wavelenght_unit":ome.Channel["ExcitationWavelengthUnit"]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def cycle_info(cycle_path,
|
|
82
|
+
platform_pattern,
|
|
83
|
+
ref_marker='DAPI'):
|
|
84
|
+
"""
|
|
85
|
+
This function reads the images produced by the MACSima device and returns the acquisition information
|
|
86
|
+
specified in the image name.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
cycle_path (Path): full path to the cycle folder
|
|
90
|
+
platform_pattern (dict): dictionary with the pattern to search in the image name.
|
|
91
|
+
ref_marker (str): marker of reference used for registration
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
df (pd.DataFrame): dataframe with the acquisition information, ROI, rack, exposure time etc.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
full_image_paths = list(cycle_path.glob("*.tif"))
|
|
98
|
+
file_names = [x.name for x in full_image_paths]
|
|
99
|
+
|
|
100
|
+
info=info_dic(platform_pattern)
|
|
101
|
+
|
|
102
|
+
info['full_path'] = full_image_paths
|
|
103
|
+
info['img_name'] = file_names
|
|
104
|
+
|
|
105
|
+
for feat,value in platform_pattern.items():
|
|
106
|
+
info[feat]=extract_values(target_pattern=value, strings=file_names,number_cast=False)
|
|
107
|
+
|
|
108
|
+
df = pd.DataFrame(info)
|
|
109
|
+
df.loc[df['filter']==ref_marker,'marker'] = ref_marker
|
|
110
|
+
|
|
111
|
+
df.insert(loc=df.shape[1], column="exposure_level", value=0)
|
|
112
|
+
df["exposure_time"] = df["exposure_time"].astype(float)
|
|
113
|
+
df["exposure_level"] = ( df.groupby(["source","marker","filter"])["exposure_time"].rank(method="dense")).astype(int)
|
|
114
|
+
|
|
115
|
+
return df
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def append_metadata(cycle_info_df):
|
|
119
|
+
"""
|
|
120
|
+
This function appends the metadata extracted from the tiff files to the cycle_info dataframe.
|
|
121
|
+
Args:
|
|
122
|
+
cycle_info_df (pd.DataFrame): dataframe with the acquisition information
|
|
123
|
+
Returns:
|
|
124
|
+
pd.DataFrame: dataframe with the metadata appended to the cycle_info dataframe as new columns.
|
|
125
|
+
"""
|
|
126
|
+
meta_feats=list( map(extract_metadata, cycle_info_df['full_path'].values) )
|
|
127
|
+
|
|
128
|
+
for key, val in merge_dicts(meta_feats).items():
|
|
129
|
+
cycle_info_df.insert(loc=cycle_info_df.shape[1], column=key, value=val)
|
|
130
|
+
|
|
131
|
+
return cycle_info_df
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def conform_markers(mf_tuple,
|
|
135
|
+
ref_marker='DAPI'):
|
|
136
|
+
"""
|
|
137
|
+
This function reorders the markers in the mf_tuple so that the reference marker is the first element.
|
|
138
|
+
Args:
|
|
139
|
+
mf_tuple (tuple): tuple with the markers and filters
|
|
140
|
+
ref_marker (str): reference marker used for registration
|
|
141
|
+
Returns:
|
|
142
|
+
list: list with the markers and filters reordered so that the reference marker is the first element.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
markers = [tup for tup in mf_tuple if tup[0]!=ref_marker]
|
|
146
|
+
markers.insert(0,(ref_marker,ref_marker))
|
|
147
|
+
return markers
|
|
148
|
+
|
|
149
|
+
def init_stack(group,
|
|
150
|
+
no_of_channels):
|
|
151
|
+
"""
|
|
152
|
+
This function initializes the stack array with the dimensions of the tiles.
|
|
153
|
+
Args:
|
|
154
|
+
ref_tile_index (int): index of the reference tile
|
|
155
|
+
groupby_obj (pd.DataFrame.groupby): groupby object with the tiles
|
|
156
|
+
marker_filter_map (list): list with the markers and filters
|
|
157
|
+
Returns:
|
|
158
|
+
np.ndarray: array with the dimensions of the stack array (depth, height, width) and the dtype of the
|
|
159
|
+
reference tile.
|
|
160
|
+
"""
|
|
161
|
+
aux_array=[ group['size_x'].unique() , group['size_y'].unique(), group['type'].unique() ]
|
|
162
|
+
check_array=np.array( [ len(element) for element in aux_array ] )
|
|
163
|
+
if np.any(check_array>1):
|
|
164
|
+
print("Warning:tiles of these acquisition have no unique value for the following columns: xy-size or data type")
|
|
165
|
+
width, height, data_type = [ element[0] for element in aux_array ]
|
|
166
|
+
total_tiles = group['tile'].nunique()
|
|
167
|
+
depth = total_tiles * no_of_channels
|
|
168
|
+
|
|
169
|
+
stack = np.zeros( (depth,int(height),int(width)), dtype=data_type)
|
|
170
|
+
|
|
171
|
+
return stack
|
|
172
|
+
|
|
173
|
+
def cast_stack_name(cycle_no,
|
|
174
|
+
acq_group_index,
|
|
175
|
+
marker_filter_map):
|
|
176
|
+
"""
|
|
177
|
+
This function creates the name of the stack file.
|
|
178
|
+
Args:
|
|
179
|
+
cycle_no (int): cycle number
|
|
180
|
+
acq_group_index (tuple): tuple with the acquisition information
|
|
181
|
+
marker_filter_map (list): list with the markers and filters
|
|
182
|
+
Returns:
|
|
183
|
+
str: name of the stack file.
|
|
184
|
+
"""
|
|
185
|
+
markers='__'.join([element[0] for element in marker_filter_map ])
|
|
186
|
+
filters='__'.join([element[1] for element in marker_filter_map ])
|
|
187
|
+
cycle_no = int(cycle_no)
|
|
188
|
+
|
|
189
|
+
c = f'{cycle_no:03d}'
|
|
190
|
+
s = acq_group_index[0]
|
|
191
|
+
e = acq_group_index[4]
|
|
192
|
+
r = acq_group_index[1]
|
|
193
|
+
w = acq_group_index[2]
|
|
194
|
+
roi = acq_group_index[3]
|
|
195
|
+
m = markers
|
|
196
|
+
f = filters
|
|
197
|
+
img_format = 'ome.tiff'
|
|
198
|
+
|
|
199
|
+
# Nicer way to format strings
|
|
200
|
+
name = f'cycle-{c}-src-{s}-rack-{r}-well-{w}-roi-{roi}-exp-{e}-markers-{m}-filters-{f}.{img_format}'
|
|
201
|
+
|
|
202
|
+
return name
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def cast_outdir_name(tup):
|
|
206
|
+
"""
|
|
207
|
+
This function creates the name of the output directory.
|
|
208
|
+
Args:
|
|
209
|
+
tup (tuple): tuple with the acquisition information
|
|
210
|
+
Returns:
|
|
211
|
+
str: name of the output directory.
|
|
212
|
+
"""
|
|
213
|
+
r = tup[1]
|
|
214
|
+
w = tup[2]
|
|
215
|
+
roi = tup[3]
|
|
216
|
+
e = tup[4]
|
|
217
|
+
|
|
218
|
+
# Nicer way to format strings
|
|
219
|
+
name = f'rack-{r}-well-{w}-roi-{roi}-exp-{e}'
|
|
220
|
+
|
|
221
|
+
return name
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def outputs_dic():
|
|
225
|
+
"""
|
|
226
|
+
This function initializes the dictionary used to store the outputs of the create_stack function.
|
|
227
|
+
Returns:
|
|
228
|
+
dict: dictionary with the keys 'index', 'array', 'full_path', 'ome' and empty lists as values
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
out={
|
|
232
|
+
'index':[],
|
|
233
|
+
'array':[],
|
|
234
|
+
'full_path':[],
|
|
235
|
+
'ome':[],
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return out
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def select_by_exposure(list_indices,
|
|
242
|
+
exp_index,
|
|
243
|
+
target='max'):
|
|
244
|
+
"""
|
|
245
|
+
This function selects the indices with the maximum or minimum exposure time.
|
|
246
|
+
Args:
|
|
247
|
+
list_indices (list): list of indices
|
|
248
|
+
exp_index (int): index of the exposure time
|
|
249
|
+
target (str): 'max' or 'min'
|
|
250
|
+
Returns:
|
|
251
|
+
list: list of selected indices
|
|
252
|
+
"""
|
|
253
|
+
selected_indices = []
|
|
254
|
+
df_aux = pd.DataFrame( np.row_stack(list_indices) )
|
|
255
|
+
group_by_indices = np.setdiff1d( range(0, len(list_indices[0]) ), exp_index ).tolist()
|
|
256
|
+
|
|
257
|
+
for key, frame in df_aux.groupby( group_by_indices ):
|
|
258
|
+
if target == 'max':
|
|
259
|
+
selected_indices.append( key + ( int(frame[exp_index].max() ), ) )
|
|
260
|
+
elif target == 'min':
|
|
261
|
+
selected_indices.append( key + ( int( frame[exp_index].min()), ) )
|
|
262
|
+
|
|
263
|
+
return selected_indices
|
|
264
|
+
|
|
265
|
+
def append_reference(frame,frame_index,groups,aux_reference_exp_level,ref_marker_name):
|
|
266
|
+
|
|
267
|
+
aux_index = list(frame_index)
|
|
268
|
+
aux_index[-1] = aux_reference_exp_level
|
|
269
|
+
aux_index = tuple(aux_index)
|
|
270
|
+
aux_group = groups.get_group(aux_index)
|
|
271
|
+
aux_group = aux_group.loc[aux_group['marker']==ref_marker_name]
|
|
272
|
+
frame_ = pd.concat([frame, aux_group])
|
|
273
|
+
return frame_
|
|
274
|
+
|
|
275
|
+
def append_missing_channels(group, exception_table ):
|
|
276
|
+
#exception_table.cols=["tile","missing_ch","channels","aux_tile"]
|
|
277
|
+
tiles=group.groupby(['tile'])
|
|
278
|
+
incomplete_tiles=exception_table.loc[exception_table['missing_ch']==True ]
|
|
279
|
+
add_tiles=[]
|
|
280
|
+
for row in incomplete_tiles.itertuples(index=False):
|
|
281
|
+
aux_tile_df=tiles.get_group( (row.aux_tile,) )
|
|
282
|
+
missing_markers=[ tuple(element.split(',')) for element in row.channels.split(':') ]
|
|
283
|
+
for marker, filt in missing_markers:
|
|
284
|
+
df=aux_tile_df.loc[ (aux_tile_df['marker']==marker) & (aux_tile_df['filter']==filt) ]
|
|
285
|
+
#df['tile']=row.tile
|
|
286
|
+
df.loc[:,'tile']=row.tile
|
|
287
|
+
add_tiles.append(df)
|
|
288
|
+
|
|
289
|
+
aux_group=pd.concat(add_tiles)
|
|
290
|
+
aux_group[ ['full_path','img_name'] ]='missing'
|
|
291
|
+
group_=pd.concat([group,aux_group])
|
|
292
|
+
|
|
293
|
+
return group_
|
|
294
|
+
|
|
295
|
+
def conform_acquisition_group(group,conformed_markers):
|
|
296
|
+
aux=[]
|
|
297
|
+
for tile_id,frame in group.groupby(['tile']):
|
|
298
|
+
aux.extend([ frame.loc[ (frame['marker']==marker) & (frame['filter']==filt)] for marker, filt in conformed_markers ])
|
|
299
|
+
group_conformed=pd.concat(aux)
|
|
300
|
+
|
|
301
|
+
return group_conformed
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def create_stack(cycle_info_df,
|
|
306
|
+
output_dir,
|
|
307
|
+
ref_marker='DAPI',
|
|
308
|
+
hi_exp=False,
|
|
309
|
+
ill_corr=False,
|
|
310
|
+
out_folder='raw',
|
|
311
|
+
extended_outputs=False):
|
|
312
|
+
"""
|
|
313
|
+
This function creates the stack of images from the cycle_info dataframe.
|
|
314
|
+
Args:
|
|
315
|
+
cycle_info_df (pd.DataFrame): dataframe with the acquisition information
|
|
316
|
+
output_dir (Path): full path to the output directory
|
|
317
|
+
ref_marker (str): reference marker used for registration
|
|
318
|
+
hi_exp (bool): if True, only the tiles with the highest exposure time are selected
|
|
319
|
+
ill_corr (bool): if True, the illumination correction is applied
|
|
320
|
+
out_folder (str): name of the output folder
|
|
321
|
+
extended_outputs (bool): if True, the function returns a dictionary with the stack arrays, full paths and ome-xml metadata
|
|
322
|
+
Returns:
|
|
323
|
+
np.ndarray or list: stack array or list with the full paths of the stack files created in the output directory.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
if extended_outputs:
|
|
327
|
+
out = outputs_dic()
|
|
328
|
+
else:
|
|
329
|
+
out = {'output_paths':[]}
|
|
330
|
+
|
|
331
|
+
#'exposure_level' should always be the last element of the dimensions list
|
|
332
|
+
dimensions=['source','rack','well','roi','exposure_level']
|
|
333
|
+
|
|
334
|
+
acq_group = cycle_info_df.groupby(dimensions)
|
|
335
|
+
acq_index = list( acq_group.indices.keys() )
|
|
336
|
+
expt_matrix_roi=expt.at_roi(acq_group,dimensions,ref_marker).groupby(dimensions)#exceptions matrix
|
|
337
|
+
|
|
338
|
+
if hi_exp:
|
|
339
|
+
exp_level_index=np.argwhere( np.asarray(dimensions)=='exposure_level' ).flatten()[0]
|
|
340
|
+
acq_index = select_by_exposure(acq_index,exp_level_index,target='max')
|
|
341
|
+
|
|
342
|
+
for index in acq_index:
|
|
343
|
+
stack_output_dir = output_dir / cast_outdir_name(index) / out_folder
|
|
344
|
+
stack_output_dir.mkdir(parents=True, exist_ok=True)
|
|
345
|
+
group = acq_group.get_group(index)
|
|
346
|
+
|
|
347
|
+
exist_ref, aux_reference= expt_matrix_roi.get_group(index )[ ['ref_marker','aux_exp_level'] ].iloc[0].values
|
|
348
|
+
if not exist_ref:
|
|
349
|
+
group=append_reference(group, index, acq_group, aux_reference, ref_marker)
|
|
350
|
+
#extract list of unique pairs (marker,filter)
|
|
351
|
+
marker_filter_map=group[['marker','filter']].value_counts().index.values
|
|
352
|
+
#conform the pairs (marker,filter) as to have the reference marker in the first place (1st channel) of the list
|
|
353
|
+
conformed_markers = conform_markers(marker_filter_map, ref_marker)
|
|
354
|
+
|
|
355
|
+
expt_matrix_tiles=expt.at_tile(group,conformed_markers)
|
|
356
|
+
if expt_matrix_tiles['missing_ch'].any():
|
|
357
|
+
group=append_missing_channels(group,expt_matrix_tiles)
|
|
358
|
+
|
|
359
|
+
stack = init_stack(group, len( conformed_markers))
|
|
360
|
+
conformed_group=conform_acquisition_group(group,conformed_markers)
|
|
361
|
+
#ome = ome_writer.create_ome(group, conformed_markers)
|
|
362
|
+
ome = ome_writer.create_ome(conformed_group, conformed_markers)
|
|
363
|
+
counter = 0
|
|
364
|
+
groups_of_tiles = conformed_group.groupby(['tile'])
|
|
365
|
+
#for tile_no, frame in groups_of_tiles:
|
|
366
|
+
# for marker, filter in conformed_markers:
|
|
367
|
+
# target_path = frame.loc[ (frame['marker']==marker) & (frame['filter']==filter) ].full_path.values[0]
|
|
368
|
+
# stack[counter,:,:] = tifff.imread(Path(target_path))
|
|
369
|
+
# counter += 1
|
|
370
|
+
for tile_id,frame in groups_of_tiles:
|
|
371
|
+
for img_path in frame['full_path'].values:
|
|
372
|
+
try:
|
|
373
|
+
stack[counter,:,:] = tifff.imread(Path(img_path))
|
|
374
|
+
except:
|
|
375
|
+
if img_path=='missing':
|
|
376
|
+
pass
|
|
377
|
+
counter += 1
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
stack_name = cast_stack_name(frame.cycle.iloc[0], index, conformed_markers)
|
|
382
|
+
|
|
383
|
+
if ill_corr:
|
|
384
|
+
tag = 'corr_'
|
|
385
|
+
no_of_channels = len(conformed_markers)
|
|
386
|
+
stack = illumination_corr.apply_corr(stack,no_of_channels)
|
|
387
|
+
else:
|
|
388
|
+
tag = ''
|
|
389
|
+
|
|
390
|
+
stack_file_path = stack_output_dir / f'{tag}{stack_name}'
|
|
391
|
+
|
|
392
|
+
if extended_outputs:
|
|
393
|
+
out['index'].append(index)
|
|
394
|
+
out['array'].append(stack)
|
|
395
|
+
out['full_path'].append(stack_file_path)
|
|
396
|
+
out['ome'].append(ome)
|
|
397
|
+
else:
|
|
398
|
+
out['output_paths'].append(stack_output_dir)
|
|
399
|
+
tifff.imwrite( stack_file_path , stack, photometric='minisblack' )
|
|
400
|
+
ome,ome_xml = ome_writer.create_ome(group, conformed_markers)
|
|
401
|
+
tifff.tiffcomment(stack_file_path, ome_xml)
|
|
402
|
+
|
|
403
|
+
if extended_outputs:
|
|
404
|
+
return out
|
|
405
|
+
else:
|
|
406
|
+
return np.unique( out['output_paths'] )
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Schapiro Lab
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: macsima2mc
|
|
3
|
+
Version: 1.2.3
|
|
4
|
+
Summary: stages MACSima tiles for registration with ASHLAR in MCMICRO.
|
|
5
|
+
Author-email: Victor Perez <hhabraxas@gmail.com>, "Miguel A. Ibarra-Arellano" <c180l058j@mozmail.com>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/SchapiroLabor/macsima2mc
|
|
8
|
+
Project-URL: Issues, https://github.com/SchapiroLabor/macsima2mc/issues
|
|
9
|
+
Keywords: mcmicro,multiplex imaging,proteomics,macsima,staging
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: ome-types==0.5.2
|
|
18
|
+
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: tifffile
|
|
20
|
+
Requires-Dist: pandas
|
|
21
|
+
Requires-Dist: beautifulsoup4
|
|
22
|
+
Requires-Dist: scikit-image
|
|
23
|
+
Requires-Dist: BaSiCPy
|
|
24
|
+
|
|
25
|
+
# Staging module for Miltenyi - MACSIMA to MCMICRO
|
|
26
|
+
|
|
27
|
+
The macsima2mc.py script stages MACSima data sets for being registerd with ASHLAR in MCMICRO.
|
|
28
|
+
|
|
29
|
+
The script takes as main input the path to the cycle folder that cointains the raw tiles generated by the Miltenyi-MACSima platform. macsima2mc.py reads these tiles and distributes them into acquisition groups. i.e tiles with common rack, well, ROI and exposure levels will be written into an ome.tiff file with the necessary metadata for registration. To be more precise two such ome.tiff files will be generated per cycle, one corresponds to the background signal and the other to the markers signal.
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
### Required arguments
|
|
33
|
+
| Argument|Long name|Type|Description|Default value|
|
|
34
|
+
|---------|---------|----|-----------|-------------|
|
|
35
|
+
| -i | string/path | --input | Absolute path to the the parent folder of the raw tiles, i.e. the cycle folder whose name follows the pattern X_Cycle_N,where N represents the cycle number | NA |
|
|
36
|
+
| -o | string/path | --output | Absolute path to the directory in which the outputs will be saved. If the output directory doesn't exist it will be created. | NA |
|
|
37
|
+
|
|
38
|
+
### Optional arguments
|
|
39
|
+
| Argument|Type|Long name| Description | Default value |
|
|
40
|
+
|---------|----|---------|-------------|---------------|
|
|
41
|
+
|-rm|string | --reference_marker | Name of the reference marker for registration|'DAPI'|
|
|
42
|
+
|-osd|string | --output_subdir | String specifying the name of the subfolder in which the staged images will be saved.|'raw'|
|
|
43
|
+
|-ic|boolean flag | --illumination_correction |Give this flag to apply illumination correction to all tiles, the illumination profiles are calculated with basicpy | FALSE |
|
|
44
|
+
|-he|boolean flag | --hi_exposure_only |Give this flag to extract only the set of images with the highest exposure time|FALSE|
|
|
45
|
+
|-rr|boolean flag | --remove_reference_marker |mark the removal of the reference marker ,e.g. DAPI, for all cycles except the first one |FALSE|
|
|
46
|
+
|-qc|boolean flag | --qc_metrics | measure features of contrast, intensity and sharpness of each tile in the cycle and appends them to a table |FALSE|
|
|
47
|
+
|-wt|boolean flag | --write_table | writes a table with the acquisition parameters, metadata and,if enabled, qc metrics of each tile. Table will be saved in --output/cycle_info |FALSE|
|
|
48
|
+
## Container usage
|
|
49
|
+
### download container:
|
|
50
|
+
- Docker
|
|
51
|
+
```
|
|
52
|
+
docker pull ghcr.io/schapirolabor/macsima2mc:v1.1.1
|
|
53
|
+
```
|
|
54
|
+
- Singularity
|
|
55
|
+
```
|
|
56
|
+
singularity pull docker://ghcr.io/schapirolabor/macsima2mc:v1.1.1
|
|
57
|
+
```
|
|
58
|
+
### Script execution
|
|
59
|
+
|
|
60
|
+
- Singularity
|
|
61
|
+
``` bash
|
|
62
|
+
singularity exec --bind $path_to_your_local_cycle_folder:/mnt,$path_to_your_local_output_folder:/media --no-home $path_to_container python staging/macsima2mc/macsima2mc.py -i /mnt/$path_to_your_local_cycle_folder -o /media/$path_to_your_local_output_folder
|
|
63
|
+
```
|
|
64
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
macsima2mc/CLI.py,sha256=631g1-wqKMUkuwYU6Lul9OnZG8uNeDhku-B3qvzCxwY,3019
|
|
2
|
+
macsima2mc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
macsima2mc/exceptions.py,sha256=JmerNzYUx9nF684lwhUESL0WBb_Vgx_krw20RGwOXXM,2950
|
|
4
|
+
macsima2mc/illumination_corr.py,sha256=R5kaTdKpSG_NNLUkUFFb_o9ysjRo9Z_NVF7XeYSShuw,2150
|
|
5
|
+
macsima2mc/macsima2mc.py,sha256=LnHZhYYEbFOkVNEPgcg4niT_EJkkLzbFW40hOsSkn00,1740
|
|
6
|
+
macsima2mc/mc_tools.py,sha256=XYW6QZhmdmmBWlSzDU9Fx8Wyv9e1Ru_p8kUtFIi4upQ,3965
|
|
7
|
+
macsima2mc/ome_schema.py,sha256=Ub8zWW292UJ-UPPizYOym8Pdguzk_oANVVNUFvS4YNA,5263
|
|
8
|
+
macsima2mc/ome_writer.py,sha256=1mNAG2_X7qq4Et6RuA0QcMp-J1c9coHFn7NRP9Ysbx0,1163
|
|
9
|
+
macsima2mc/qc.py,sha256=GG-nteWJt1YAmNQybS141WOlp7tcA9-0u-IMpv4BR_E,1958
|
|
10
|
+
macsima2mc/templates.py,sha256=FXoFrmjXHo27fRmxDBh9SUY-t87x4D6PrcbX5DR4bmY,1974
|
|
11
|
+
macsima2mc/tools.py,sha256=7cB9j3spT7bXn7tOCBl8IIKedoht5sf0x7Yc3xqA3zg,15198
|
|
12
|
+
macsima2mc-1.2.3.dist-info/LICENSE,sha256=HGw52jD0kv8othfJyGZUgeF0ixa0nXNxpm9LYHI6Mo4,1499
|
|
13
|
+
macsima2mc-1.2.3.dist-info/METADATA,sha256=eGsuab1C4-hiDkgXd6BqMGIqNxY-MYsynzFQSX9bCh4,3708
|
|
14
|
+
macsima2mc-1.2.3.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
15
|
+
macsima2mc-1.2.3.dist-info/entry_points.txt,sha256=b5eMvcI2cGc45a4KL98gHAlwcICMTXSsi0wGEb-D8hc,58
|
|
16
|
+
macsima2mc-1.2.3.dist-info/top_level.txt,sha256=9cYr15WLXjXnOaEh4UkVoxAH_jzBYsqSpqphNaYIZ8c,11
|
|
17
|
+
macsima2mc-1.2.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
macsima2mc
|