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 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
@@ -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
+
@@ -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
@@ -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
@@ -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
+
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (76.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ macsima2mc = macsima2mc.macsima2mc:main
@@ -0,0 +1 @@
1
+ macsima2mc