def test_get_bids_path_from_fname(fname): bids_path = get_bids_path_from_fname(fname) assert bids_path.basename == Path(fname).name if '/bids_root/' in fname: assert Path(bids_path.root) == Path('/bids_root') else: if 'meg' in fname: # directory should match assert Path(bids_path.directory) == Path('sub-01/ses-02/meg') # root should be default '.' assert str(bids_path.root) == '.'
def _summarize_channels_tsv(root, scans_fpaths): """Summarize channels.tsv data in BIDS root directory. Currently, summarizes all REQUIRED components of channels data, and some RECOMMENDED and OPTIONAL components. Parameters ---------- root : str | pathlib.Path The path of the root of the BIDS compatible folder. scans_fpaths : list A list of all *_scans.tsv files in ``root``. The summary will occur for all scans listed in the *_scans.tsv files. Returns ------- template_dict : dict A dictionary of values for various template strings. """ root = Path(root) # keep track of channel type, status ch_status_count = {'bad': [], 'good': []} ch_count = [] # loop through each scan for scan_fpath in scans_fpaths: # load in the scans.tsv file # and read metadata for each scan scans_tsv = _from_tsv(scan_fpath) scans = scans_tsv['filename'] for scan in scans: # summarize metadata of recordings bids_path, _ = _parse_ext(scan) datatype = op.dirname(scan) if datatype not in ['meg', 'eeg', 'ieeg']: continue # convert to BIDS Path if not isinstance(bids_path, BIDSPath): bids_path = get_bids_path_from_fname(bids_path) bids_path.root = root # XXX: improve to allow emptyroom if bids_path.subject == 'emptyroom': continue channels_fname = _find_matching_sidecar(bids_path=bids_path, suffix='channels', extension='.tsv') # summarize channels.tsv channels_tsv = _from_tsv(channels_fname) for status in ch_status_count.keys(): ch_status = [ ch for ch in channels_tsv['status'] if ch == status ] ch_status_count[status].append(len(ch_status)) ch_count.append(len(channels_tsv['name'])) # create summary template strings for status template_dict = { 'mean_chs': np.mean(ch_count), 'std_chs': np.std(ch_count), 'mean_good_chs': np.mean(ch_status_count['good']), 'std_good_chs': np.std(ch_status_count['good']), 'mean_bad_chs': np.mean(ch_status_count['bad']), 'std_bad_chs': np.std(ch_status_count['bad']), } for key, val in template_dict.items(): template_dict[key] = round(val, 2) return template_dict
def _summarize_sidecar_json(root, scans_fpaths): """Summarize scans in BIDS root directory. Parameters ---------- root : str | pathlib.Path The path of the root of the BIDS compatible folder. scans_fpaths : list A list of all *_scans.tsv files in ``root``. The summary will occur for all scans listed in the *_scans.tsv files. Returns ------- template_dict : dict A dictionary of values for various template strings. """ n_scans = 0 powerlinefreqs, sfreqs = set(), set() manufacturers = set() length_recordings = [] # loop through each scan for scan_fpath in scans_fpaths: # load in the scans.tsv file # and read metadata for each scan scans_tsv = _from_tsv(scan_fpath) scans = scans_tsv['filename'] for scan in scans: # summarize metadata of recordings bids_path, ext = _parse_ext(scan) datatype = op.dirname(scan) if datatype not in ALLOWED_DATATYPES: continue n_scans += 1 # convert to BIDS Path if not isinstance(bids_path, BIDSPath): bids_path = get_bids_path_from_fname(bids_path) bids_path.root = root # XXX: improve to allow emptyroom if bids_path.subject == 'emptyroom': continue sidecar_fname = _find_matching_sidecar(bids_path=bids_path, suffix=datatype, extension='.json') with open(sidecar_fname, 'r', encoding='utf-8-sig') as fin: sidecar_json = json.load(fin) # aggregate metadata from each scan # REQUIRED kwargs sfreq = sidecar_json['SamplingFrequency'] powerlinefreq = str(sidecar_json['PowerLineFrequency']) software_filters = sidecar_json.get('SoftwareFilters') if not software_filters: software_filters = 'n/a' # RECOMMENDED kwargs manufacturer = sidecar_json.get('Manufacturer', 'n/a') record_duration = sidecar_json.get('RecordingDuration', 'n/a') sfreqs.add(str(np.round(sfreq, 2))) powerlinefreqs.add(str(powerlinefreq)) if manufacturer != 'n/a': manufacturers.add(manufacturer) length_recordings.append(record_duration) # XXX: length summary is only allowed, if no 'n/a' was found if any([dur == 'n/a' for dur in length_recordings]): length_recordings = None template_dict = { 'n_scans': n_scans, 'manufacturer': list(manufacturers), 'sfreq': sfreqs, 'powerlinefreq': powerlinefreqs, 'software_filters': software_filters, 'length_recordings': length_recordings, } return template_dict
def get_head_mri_trans(bids_path, extra_params=None, t1_bids_path=None, fs_subject=None, fs_subjects_dir=None, *, kind=None, verbose=None): """Produce transformation matrix from MEG and MRI landmark points. Will attempt to read the landmarks of Nasion, LPA, and RPA from the sidecar files of (i) the MEG and (ii) the T1-weighted MRI data. The two sets of points will then be used to calculate a transformation matrix from head coordinates to MRI coordinates. .. note:: The MEG and MRI data need **not** necessarily be stored in the same session or even in the same BIDS dataset. See the ``t1_bids_path`` parameter for details. Parameters ---------- bids_path : BIDSPath The path of the electrophysiology recording. If ``datatype`` and ``suffix`` are not present, they will be set to ``'meg'``, and a warning will be raised. .. versionchanged:: 0.10 A warning is raised it ``datatype`` or ``suffix`` are not set. extra_params : None | dict Extra parameters to be passed to :func:`mne.io.read_raw` when reading the MEG file. t1_bids_path : BIDSPath | None If ``None`` (default), will try to discover the T1-weighted MRI file based on the name and location of the MEG recording specified via the ``bids_path`` parameter. Alternatively, you explicitly specify which T1-weighted MRI scan to use for extraction of MRI landmarks. To do that, pass a :class:`mne_bids.BIDSPath` pointing to the scan. Use this parameter e.g. if the T1 scan was recorded during a different session than the MEG. It is even possible to point to a T1 image stored in an entirely different BIDS dataset than the MEG data. fs_subject : str The subject identifier used for FreeSurfer. .. versionchanged:: 0.10 Does not default anymore to ``bids_path.subject`` if ``None``. fs_subjects_dir : path-like | None The FreeSurfer subjects directory. If ``None``, defaults to the ``SUBJECTS_DIR`` environment variable. .. versionadded:: 0.8 kind : str | None The suffix of the anatomical landmark names in the JSON sidecar. A suffix might be present e.g. to distinguish landmarks between sessions. If provided, should not include a leading underscore ``_``. For example, if the landmark names in the JSON sidecar file are ``LPA_ses-1``, ``RPA_ses-1``, ``NAS_ses-1``, you should pass ``'ses-1'`` here. If ``None``, no suffix is appended, the landmarks named ``Nasion`` (or ``NAS``), ``LPA``, and ``RPA`` will be used. .. versionadded:: 0.10 %(verbose)s Returns ------- trans : mne.transforms.Transform The data transformation matrix from head to MRI coordinates. """ if not has_nibabel(): # pragma: no cover raise ImportError('This function requires nibabel.') import nibabel as nib if not isinstance(bids_path, BIDSPath): raise RuntimeError('"bids_path" must be a BIDSPath object. Please ' 'instantiate using mne_bids.BIDSPath().') # check root available meg_bids_path = bids_path.copy() del bids_path if meg_bids_path.root is None: raise ValueError('The root of the "bids_path" must be set. ' 'Please use `bids_path.update(root="<root>")` ' 'to set the root of the BIDS folder to read.') # if the bids_path is underspecified, only get info for MEG data if meg_bids_path.datatype is None: meg_bids_path.datatype = 'meg' warn( 'bids_path did not have a datatype set. Assuming "meg". This ' 'will raise an exception in the future.', module='mne_bids', category=DeprecationWarning) if meg_bids_path.suffix is None: meg_bids_path.suffix = 'meg' warn( 'bids_path did not have a suffix set. Assuming "meg". This ' 'will raise an exception in the future.', module='mne_bids', category=DeprecationWarning) # Get the sidecar file for MRI landmarks t1w_bids_path = ((meg_bids_path if t1_bids_path is None else t1_bids_path).copy().update(datatype='anat', suffix='T1w', task=None)) t1w_json_path = _find_matching_sidecar(bids_path=t1w_bids_path, extension='.json', on_error='ignore') del t1_bids_path if t1w_json_path is not None: t1w_json_path = Path(t1w_json_path) if t1w_json_path is None or not t1w_json_path.exists(): raise FileNotFoundError( f'Did not find T1w JSON sidecar file, tried location: ' f'{t1w_json_path}') for extension in ('.nii', '.nii.gz'): t1w_path_candidate = t1w_json_path.with_suffix(extension) if t1w_path_candidate.exists(): t1w_bids_path = get_bids_path_from_fname(fname=t1w_path_candidate) break if not t1w_bids_path.fpath.exists(): raise FileNotFoundError( f'Did not find T1w recording file, tried location: ' f'{t1w_path_candidate.name.replace(".nii.gz", "")}[.nii, .nii.gz]') # Get MRI landmarks from the JSON sidecar t1w_json = json.loads(t1w_json_path.read_text(encoding='utf-8')) mri_coords_dict = t1w_json.get('AnatomicalLandmarkCoordinates', dict()) # landmarks array: rows: [LPA, NAS, RPA]; columns: [x, y, z] suffix = f"_{kind}" if kind is not None else "" mri_landmarks = np.full((3, 3), np.nan) for landmark_name, coords in mri_coords_dict.items(): if landmark_name.upper() == ('LPA' + suffix).upper(): mri_landmarks[0, :] = coords elif landmark_name.upper() == ('RPA' + suffix).upper(): mri_landmarks[2, :] = coords elif (landmark_name.upper() == ('NAS' + suffix).upper() or landmark_name.lower() == ('nasion' + suffix).lower()): mri_landmarks[1, :] = coords else: continue if np.isnan(mri_landmarks).any(): raise RuntimeError( f'Could not extract fiducial points from T1w sidecar file: ' f'{t1w_json_path}\n\n' f'The sidecar file SHOULD contain a key ' f'"AnatomicalLandmarkCoordinates" pointing to an ' f'object with the keys "LPA", "NAS", and "RPA". ' f'Yet, the following structure was found:\n\n' f'{mri_coords_dict}') # The MRI landmarks are in "voxels". We need to convert them to the # Neuromag RAS coordinate system in order to compare them with MEG # landmarks. See also: `mne_bids.write.write_anat` if fs_subject is None: warn( 'Passing "fs_subject=None" has been deprecated and will raise ' 'an error in future versions. Please explicitly specify the ' 'FreeSurfer subject name.', DeprecationWarning) fs_subject = f'sub-{meg_bids_path.subject}' fs_subjects_dir = get_subjects_dir(fs_subjects_dir, raise_error=False) fs_t1_path = Path(fs_subjects_dir) / fs_subject / 'mri' / 'T1.mgz' if not fs_t1_path.exists(): raise ValueError( f"Could not find {fs_t1_path}. Consider running FreeSurfer's " f"'recon-all` for subject {fs_subject}.") fs_t1_mgh = nib.load(str(fs_t1_path)) t1_nifti = nib.load(str(t1w_bids_path.fpath)) # Convert to MGH format to access vox2ras method t1_mgh = nib.MGHImage(t1_nifti.dataobj, t1_nifti.affine) # convert to scanner RAS mri_landmarks = apply_trans(t1_mgh.header.get_vox2ras(), mri_landmarks) # convert to FreeSurfer T1 voxels (same scanner RAS as T1) mri_landmarks = apply_trans(fs_t1_mgh.header.get_ras2vox(), mri_landmarks) # now extract transformation matrix and put back to RAS coordinates of MRI vox2ras_tkr = fs_t1_mgh.header.get_vox2ras_tkr() mri_landmarks = apply_trans(vox2ras_tkr, mri_landmarks) mri_landmarks = mri_landmarks * 1e-3 # Get MEG landmarks from the raw file _, ext = _parse_ext(meg_bids_path) if extra_params is None: extra_params = dict() if ext == '.fif': extra_params['allow_maxshield'] = True raw = read_raw_bids(bids_path=meg_bids_path, extra_params=extra_params) if (raw.get_montage() is None or raw.get_montage().get_positions() is None or any([ raw.get_montage().get_positions()[fid_key] is None for fid_key in ('nasion', 'lpa', 'rpa') ])): raise RuntimeError( f'Could not extract fiducial points from ``raw`` file: ' f'{meg_bids_path}\n\n' f'The ``raw`` file SHOULD contain digitization points ' 'for the nasion and left and right pre-auricular points ' 'but none were found') pos = raw.get_montage().get_positions() meg_landmarks = np.asarray((pos['lpa'], pos['nasion'], pos['rpa'])) # Given the two sets of points, fit the transform trans_fitted = fit_matched_points(src_pts=meg_landmarks, tgt_pts=mri_landmarks) trans = mne.transforms.Transform(fro='head', to='mri', trans=trans_fitted) return trans