def fmap_echotimes(src_phase_json_fname): """ Extract TE1 and TE2 from mag and phase MEGE fieldmap pairs :param src_phase_json_fname: str :return: """ # Init returned TEs te1, te2 = 0.0, 0.0 if os.path.isfile(src_phase_json_fname): # Read phase image metadata phase_dict = bio.read_json(src_phase_json_fname) # Populate series info dictionary from dcm2niix output filename info = bio.parse_dcm2niix_fname(src_phase_json_fname) # Siemens: Magnitude 1 series number is one less than phasediff series number mag1_ser_no = str(int(info['SerNo']) - 1) # Construct dcm2niix mag1 JSON filename # Requires dicm2niix v1.0.20180404 or later for echo number suffix '_e1' src_mag1_json_fname = info['SubjName'] + '--' + info['SerDesc'] + '--' + \ info['SeqName'] + '--' + mag1_ser_no + '_e1.json' src_mag1_json_path = os.path.join( os.path.dirname(src_phase_json_fname), src_mag1_json_fname) # Read mag1 metadata mag1_dict = bio.read_json(src_mag1_json_path) # Add te1 key and rename TE2 key if mag1_dict: te1 = mag1_dict['EchoTime'] te2 = phase_dict['EchoTime'] else: print( '*** Could not determine echo times multiecho fieldmap - using 0.0 ' ) else: print('* Fieldmap phase difference sidecar not found : ' + src_phase_json_fname) return te1, te2
def auto_run_no(file_list, prot_dict): """ Search for duplicate series names in dcm2niix output file list Return inferred run numbers accounting for duplication and multiple recons from single acquisition NOTES: - Multiple recons generated by single acquisition (eg multiecho fieldmaps, localizers, etc) are handled through the dcm2niix extensions (_e1, _ph, _i00001, etc). - Series number resets following subject re-landmarking make the SerNo useful only for determining series uniqueness and not for ordering or run numbering. :param file_list: list of str Nifti file name list :param prot_dict: dictionary Protocol translation dictionary :return: run_num, array of int """ # Construct list of series descriptions and original numbers from file names desc_list = [] for fname in file_list: # Parse dcm2niix filename into relevant keys, including suffix info = bio.parse_dcm2niix_fname(fname) _, bids_suffix, _ = prot_dict[info['SerDesc']] # Construct a unique series description using multirecon suffix ser_suffix = bids_suffix + '_' + info['Suffix'] # Add to list desc_list.append(ser_suffix) # Find unique ser_desc entries using sets unique_descs = set(desc_list) run_no = np.zeros(len(file_list)) for unique_desc in unique_descs: run_count = 1 for i, desc in enumerate(desc_list): if desc == unique_desc: run_no[i] = run_count run_count += 1 return run_no
def fmap_echotimes(src_phase_json_fname): """ Extract TE1 and TE2 from mag and phase MEGE fieldmap pairs :param src_phase_json_fname: str :return: """ # Init returned TEs te1, te2 = 0.0, 0.0 if os.path.isfile(src_phase_json_fname): # Read phase image metadata phase_dict = bio.read_json(src_phase_json_fname) # Populate series info dictionary from dcm2niix output filename info = bio.parse_dcm2niix_fname(src_phase_json_fname) # Siemens: Magnitude 1 series number is one less than phasediff series number mag1_ser_no = str(int(info['SerNo']) - 1) # Construct dcm2niix mag1 JSON filename # Requires dicm2niix v1.0.20180404 or later for echo number suffix '_e1' src_mag1_json_fname = info['SubjName'] + '--' + info['SerDesc'] + '--' + \ info['SeqName'] + '--' + mag1_ser_no + '_e1.json' src_mag1_json_path = os.path.join(os.path.dirname(src_phase_json_fname), src_mag1_json_fname) # Read mag1 metadata mag1_dict = bio.read_json(src_mag1_json_path) # Add te1 key and rename TE2 key if mag1_dict: te1 = mag1_dict['EchoTime'] te2 = phase_dict['EchoTime'] else: print('*** Could not determine echo times multiecho fieldmap - using 0.0 ') else: print('* Fieldmap phase difference sidecar not found : ' + src_phase_json_fname) return te1, te2
def organize_series(conv_dir, first_pass, prot_dict, src_dir, sid, ses, clean_conv_dir, overwrite=False): """ Organize dcm2niix output into BIDS subject/session directory :param conv_dir: string Working conversion directory :param first_pass: boolean Flag for first pass conversion :param prot_dict: dictionary Protocol translation dictionary :param src_dir: string BIDS source output subj or subj/session directory :param sid: string subject ID :param ses: string session name or number :param clean_conv_dir: bool clean up conversion directory :param overwrite: bool overwrite flag :return: """ # Flag for working conversion directory cleanup do_cleanup = clean_conv_dir # Proceed if conversion directory exists if os.path.isdir(conv_dir): # Get Nifti file list ordered by acquisition time nii_list, json_list, acq_times = btr.ordered_file_list(conv_dir) # Infer run numbers accounting for duplicates. # Only used if run-* not present in translator BIDS filename stub if not first_pass: run_no = btr.auto_run_no(nii_list, prot_dict) # Loop over all Nifti files (*.nii, *.nii.gz) for this subject for fc, src_nii_fname in enumerate(nii_list): # Parse image filename into fields info = bio.parse_dcm2niix_fname(src_nii_fname) # Check if we're creating new protocol dictionary if first_pass: print(' Adding protocol %s to dictionary template' % info['SerDesc']) # Add current protocol to protocol dictionary # Use default EXCLUDE_* values which can be changed (or not) by the user prot_dict[info['SerDesc']] = [ "EXCLUDE_BIDS_Directory", "EXCLUDE_BIDS_Name", "UNASSIGNED" ] else: # JSON sidecar for this image src_json_fname = json_list[fc] # Warn if not found and continue if not os.path.isfile(src_json_fname): print('* WARNING: JSON sidecar %s not found' % src_json_fname) continue if info['SerDesc'] in prot_dict.keys(): if prot_dict[info['SerDesc']][0].startswith('EXCLUDE'): # Skip excluded protocols print('* Excluding protocol ' + str(info['SerDesc'])) else: print(' Organizing ' + str(info['SerDesc'])) # Use protocol dictionary to determine purpose folder, BIDS filename suffix and fmap linking bids_purpose, bids_suffix, bids_intendedfor = prot_dict[ info['SerDesc']] # Safely add run-* key to BIDS suffix bids_suffix = btr.add_run_number( bids_suffix, run_no[fc]) # Assume the IntendedFor field should aslo have a run- added prot_dict = btr.add_intended_run( prot_dict, info, run_no[fc]) # Create BIDS purpose directory bids_purpose_dir = os.path.join(src_dir, bids_purpose) bio.safe_mkdir(bids_purpose_dir) # Complete BIDS filenames for image and sidecar if ses: bids_prefix = 'sub-' + sid + '_ses-' + ses + '_' else: bids_prefix = 'sub-' + sid + '_' # Construct BIDS source Nifti and JSON filenames bids_nii_fname = os.path.join( bids_purpose_dir, bids_prefix + bids_suffix + '.nii.gz') bids_json_fname = bids_nii_fname.replace( '.nii.gz', '.json') # Add prefix and suffix to IntendedFor values if 'UNASSIGNED' not in bids_intendedfor: if isinstance(bids_intendedfor, str): # Single linked image bids_intendedfor = btr.build_intendedfor( sid, ses, bids_intendedfor) else: # Loop over all linked images for ifc, ifstr in enumerate(bids_intendedfor): # Avoid multiple substitutions if '.nii.gz' not in ifstr: bids_intendedfor[ ifc] = btr.build_intendedfor( sid, ses, ifstr) # Special handling for specific purposes (anat, func, fmap, etc) # This function populates BIDS structure with the image and adjusted sidecar btr.purpose_handling(bids_purpose, bids_intendedfor, info['SeqName'], src_nii_fname, src_json_fname, bids_nii_fname, bids_json_fname, overwrite) else: # Skip protocols not in the dictionary print('* Protocol ' + str(info['SerDesc']) + ' is not in the dictionary, did not convert.') if not first_pass: # Optional working directory cleanup after Pass 2 if do_cleanup: print(' Cleaning up temporary files') shutil.rmtree(conv_dir) else: print(' Preserving conversion directory')
def purpose_handling(bids_purpose, bids_intendedfor, seq_name, work_nii_fname, work_json_fname, bids_nii_fname, bids_json_fname, overwrite=False): """ Special handling for each image purpose (func, anat, fmap, dwi, etc) :param bids_purpose: str :param bids_intendedfor: str :param seq_name: str :param work_nii_fname: str :param work_json_fname: str :param bids_nii_fname: str :param bids_json_fname: str :param overwrite: bool :return: """ # Init DWI sidecars work_bval_fname = [] work_bvec_fname = [] bids_bval_fname = [] bids_bvec_fname = [] # Load the JSON sidecar bids_info = bio.read_json(work_json_fname) if bids_purpose == 'func': if seq_name == 'EP': print(' EPI detected') create_events_template(bids_nii_fname, overwrite) # Add taskname to BIDS JSON sidecar bids_keys = bio.parse_bids_fname(bids_nii_fname) if 'task' in bids_keys: bids_info['TaskName'] = bids_keys['task'] else: bids_info['TaskName'] = 'unknown' elif bids_purpose == 'fmap': # Add IntendedFor field if requested through protocol translator if 'UNASSIGNED' not in bids_intendedfor: bids_info['IntendedFor'] = bids_intendedfor # Check for MEGE vs SE-EPI fieldmap images # MEGE will have a 'GR' sequence, SE-EPI will have 'EP' print(' Identifying fieldmap image type') if seq_name == 'GR': print(' GRE detected') print(' Identifying magnitude and phase images') # Siemens: Dual gradient echo fieldmaps reconstruct to three series # (Requires dcm2nixx v1.0.20180404 or later for echo number suffix) # *--GR--<serno>_e1.<ext> : magnitude image from echo 1 # *--GR--<serno>_e2.<ext> : magnitude image from echo 2 # *--GR--<serno+1>_ph.<ext> : inter-echo phase difference # Pull dcm2niix filename info work_info = bio.parse_dcm2niix_fname(work_nii_fname) if 'e1' in work_info['Suffix']: print(' Echo 1 magnitude detected') # Replace existing contrast suffix (if any) with '_magnitude1' bids_nii_fname = replace_contrast(bids_nii_fname, 'magnitude1') bids_json_fname = [] # Do not copy sidecar elif 'e2' in work_info['Suffix']: print(' Echo 2 magnitude detected') # Replace existing contrast suffix (if any) with '_magnitude1' bids_nii_fname = replace_contrast(bids_nii_fname, 'magnitude2') bids_json_fname = [] # Do not copy sidecar elif 'ph' in work_info['Suffix']: print(' Interecho phase difference detected') # Replace existing contrast suffix (if any) with '_phasediff' bids_nii_fname = replace_contrast(bids_nii_fname, 'phasediff') bids_json_fname = replace_contrast(bids_json_fname, 'phasediff') # Extract TE1 and TE2 from mag and phase JSON sidecars te1, te2 = fmap_echotimes(work_json_fname) bids_info['EchoTime1'] = te1 bids_info['EchoTime2'] = te2 else: print('* Magnitude or phase image not found - skipping') bids_nii_fname = [] bids_json_fname = [] elif seq_name == 'EP': print(' EPI detected') else: print(' Unrecognized fieldmap detected') print(' Simply copying image and sidecar to fmap directory') elif bids_purpose == 'anat': if seq_name == 'GR_IR': print( ' IR-prepared GRE detected - likely T1w MP-RAGE or equivalent' ) elif seq_name == 'SE': print(' Spin echo detected - likely T1w or T2w anatomic image') elif seq_name == 'GR': print(' Gradient echo detected') elif bids_purpose == 'dwi': # Fill DWI bval and bvec working and source filenames # Non-empty filenames trigger the copy below work_bval_fname = str(work_json_fname.replace('.json', '.bval')) bids_bval_fname = str(bids_json_fname.replace('dwi.json', 'dwi.bval')) work_bvec_fname = str(work_json_fname.replace('.json', '.bvec')) bids_bvec_fname = str(bids_json_fname.replace('dwi.json', 'dwi.bvec')) # Populate BIDS source directory with Nifti images, JSON and DWI sidecars print(' Populating BIDS source directory') if bids_nii_fname: bio.safe_copy(work_nii_fname, str(bids_nii_fname), overwrite) if bids_json_fname: bio.write_json(bids_json_fname, bids_info, overwrite) if bids_bval_fname: bio.safe_copy(work_bval_fname, bids_bval_fname, overwrite) if bids_bvec_fname: bio.safe_copy(work_bvec_fname, bids_bvec_fname, overwrite)
def organize_series(conv_dir, first_pass, prot_dict, src_dir, sid, ses, clean_conv_dir, overwrite=False): """ Organize dcm2niix output into BIDS subject/session directory :param conv_dir: string Working conversion directory :param first_pass: boolean Flag for first pass conversion :param prot_dict: dictionary Protocol translation dictionary :param src_dir: string BIDS source output subj or subj/session directory :param sid: string subject ID :param ses: string session name or number :param clean_conv_dir: bool clean up conversion directory :param overwrite: bool overwrite flag :return: """ # Flag for working conversion directory cleanup do_cleanup = clean_conv_dir # Proceed if conversion directory exists if os.path.isdir(conv_dir): # Get Nifti file list ordered by acquisition time nii_list, json_list, acq_times = btr.ordered_file_list(conv_dir) # Infer run numbers accounting for duplicates. # Only used if run-* not present in translator BIDS filename stub if not first_pass: run_no = btr.auto_run_no(nii_list, prot_dict) # Loop over all Nifti files (*.nii, *.nii.gz) for this subject for fc, src_nii_fname in enumerate(nii_list): # Parse image filename into fields info = bio.parse_dcm2niix_fname(src_nii_fname) # Check if we're creating new protocol dictionary if first_pass: print(' Adding protocol %s to dictionary template' % info['SerDesc']) # Add current protocol to protocol dictionary # Use default EXCLUDE_* values which can be changed (or not) by the user prot_dict[info['SerDesc']] = ["EXCLUDE_BIDS_Directory", "EXCLUDE_BIDS_Name", "UNASSIGNED"] else: # JSON sidecar for this image src_json_fname = json_list[fc] # Warn if not found and continue if not os.path.isfile(src_json_fname): print('* WARNING: JSON sidecar %s not found' % src_json_fname) continue if info['SerDesc'] in prot_dict.keys(): if prot_dict[info['SerDesc']][0].startswith('EXCLUDE'): # Skip excluded protocols print('* Excluding protocol ' + str(info['SerDesc'])) else: print(' Organizing ' + str(info['SerDesc'])) # Use protocol dictionary to determine purpose folder, BIDS filename suffix and fmap linking bids_purpose, bids_suffix, bids_intendedfor = prot_dict[info['SerDesc']] # Safely add run-* key to BIDS suffix bids_suffix = btr.add_run_number(bids_suffix, run_no[fc]) # Assume the IntendedFor field should aslo have a run- added prot_dict = btr.add_intended_run(prot_dict, info, run_no[fc]) # Create BIDS purpose directory bids_purpose_dir = os.path.join(src_dir, bids_purpose) bio.safe_mkdir(bids_purpose_dir) # Complete BIDS filenames for image and sidecar if ses: bids_prefix = 'sub-' + sid + '_ses-' + ses + '_' else: bids_prefix = 'sub-' + sid + '_' # Construct BIDS source Nifti and JSON filenames bids_nii_fname = os.path.join(bids_purpose_dir, bids_prefix + bids_suffix + '.nii.gz') bids_json_fname = bids_nii_fname.replace('.nii.gz', '.json') # Add prefix and suffix to IntendedFor values if 'UNASSIGNED' not in bids_intendedfor: if isinstance(bids_intendedfor, str): # Single linked image bids_intendedfor = btr.build_intendedfor(sid, ses, bids_intendedfor) else: # Loop over all linked images for ifc, ifstr in enumerate(bids_intendedfor): # Avoid multiple substitutions if '.nii.gz' not in ifstr: bids_intendedfor[ifc] = btr.build_intendedfor(sid, ses, ifstr) # Special handling for specific purposes (anat, func, fmap, etc) # This function populates BIDS structure with the image and adjusted sidecar btr.purpose_handling(bids_purpose, bids_intendedfor, info['SeqName'], src_nii_fname, src_json_fname, bids_nii_fname, bids_json_fname, overwrite) else: # Skip protocols not in the dictionary print('* Protocol ' + str(info['SerDesc']) + ' is not in the dictionary, did not convert.') if not first_pass: # Optional working directory cleanup after Pass 2 if do_cleanup: print(' Cleaning up temporary files') shutil.rmtree(conv_dir) else: print(' Preserving conversion directory')
def purpose_handling(bids_purpose, bids_intendedfor, seq_name, work_nii_fname, work_json_fname, bids_nii_fname, bids_json_fname, overwrite=False): """ Special handling for each image purpose (func, anat, fmap, dwi, etc) :param bids_purpose: str :param bids_intendedfor: str :param seq_name: str :param work_nii_fname: str :param work_json_fname: str :param bids_nii_fname: str :param bids_json_fname: str :param overwrite: bool :return: """ # Init DWI sidecars work_bval_fname = [] work_bvec_fname = [] bids_bval_fname = [] bids_bvec_fname = [] # Load the JSON sidecar bids_info = bio.read_json(work_json_fname) if bids_purpose == 'func': if seq_name == 'EP': print(' EPI detected') create_events_template(bids_nii_fname, overwrite) # Add taskname to BIDS JSON sidecar bids_keys = bio.parse_bids_fname(bids_nii_fname) if 'task' in bids_keys: bids_info['TaskName'] = bids_keys['task'] else: bids_info['TaskName'] = 'unknown' elif bids_purpose == 'fmap': # Add IntendedFor field if requested through protocol translator if 'UNASSIGNED' not in bids_intendedfor: bids_info['IntendedFor'] = bids_intendedfor # Check for MEGE vs SE-EPI fieldmap images # MEGE will have a 'GR' sequence, SE-EPI will have 'EP' print(' Identifying fieldmap image type') if seq_name == 'GR': print(' GRE detected') print(' Identifying magnitude and phase images') # Siemens: Dual gradient echo fieldmaps reconstruct to three series # (Requires dcm2nixx v1.0.20180404 or later for echo number suffix) # *--GR--<serno>_e1.<ext> : magnitude image from echo 1 # *--GR--<serno>_e2.<ext> : magnitude image from echo 2 # *--GR--<serno+1>_ph.<ext> : inter-echo phase difference # Pull dcm2niix filename info work_info = bio.parse_dcm2niix_fname(work_nii_fname) if 'e1' in work_info['Suffix']: print(' Echo 1 magnitude detected') # Replace existing contrast suffix (if any) with '_magnitude1' bids_nii_fname = replace_contrast(bids_nii_fname, 'magnitude1') bids_json_fname = [] # Do not copy sidecar elif 'e2' in work_info['Suffix']: print(' Echo 2 magnitude detected') # Replace existing contrast suffix (if any) with '_magnitude1' bids_nii_fname = replace_contrast(bids_nii_fname, 'magnitude2') bids_json_fname = [] # Do not copy sidecar elif 'ph' in work_info['Suffix']: print(' Interecho phase difference detected') # Replace existing contrast suffix (if any) with '_phasediff' bids_nii_fname = replace_contrast(bids_nii_fname, 'phasediff') bids_json_fname = replace_contrast(bids_json_fname, 'phasediff') # Extract TE1 and TE2 from mag and phase JSON sidecars te1, te2 = fmap_echotimes(work_json_fname) bids_info['EchoTime1'] = te1 bids_info['EchoTime2'] = te2 else: print('* Magnitude or phase image not found - skipping') bids_nii_fname = [] bids_json_fname = [] elif seq_name == 'EP': print(' EPI detected') else: print(' Unrecognized fieldmap detected') print(' Simply copying image and sidecar to fmap directory') elif bids_purpose == 'anat': if seq_name == 'GR_IR': print(' IR-prepared GRE detected - likely T1w MP-RAGE or equivalent') elif seq_name == 'SE': print(' Spin echo detected - likely T1w or T2w anatomic image') elif seq_name == 'GR': print(' Gradient echo detected') elif bids_purpose == 'dwi': # Fill DWI bval and bvec working and source filenames # Non-empty filenames trigger the copy below work_bval_fname = str(work_json_fname.replace('.json', '.bval')) bids_bval_fname = str(bids_json_fname.replace('dwi.json', 'dwi.bval')) work_bvec_fname = str(work_json_fname.replace('.json', '.bvec')) bids_bvec_fname = str(bids_json_fname.replace('dwi.json', 'dwi.bvec')) # Populate BIDS source directory with Nifti images, JSON and DWI sidecars print(' Populating BIDS source directory') if bids_nii_fname: bio.safe_copy(work_nii_fname, str(bids_nii_fname), overwrite) if bids_json_fname: bio.write_json(bids_json_fname, bids_info, overwrite) if bids_bval_fname: bio.safe_copy(work_bval_fname, bids_bval_fname, overwrite) if bids_bvec_fname: bio.safe_copy(work_bvec_fname, bids_bvec_fname, overwrite)