"""Update BIDS directory structures and sidecar files meta data."""# Authors: Adam Li <adam2392@gmail.com># Austin Hurst <mynameisaustinhurst@gmail.com># Stefan Appelhoff <stefan.appelhoff@mailbox.org># mne-bids developers## License: BSD-3-ClauseimportjsonfromcollectionsimportOrderedDictimportnumpyasnpfrommne.channelsimportDigMontage,make_dig_montagefrommne.ioimportread_fiducialsfrommne.io.constantsimportFIFFfrommne.utilsimport_check_on_missing,_on_missing,_validate_type,logger,verbosefrommne_bidsimportBIDSPathfrommne_bids.utilsimport_write_json# TODO: add support for tsv files
[docs]@verbosedefupdate_sidecar_json(bids_path,entries,verbose=None):"""Update sidecar files using a dictionary or JSON file. Will update metadata fields inside the path defined by ``bids_path.fpath`` according to the ``entries``. If a field does not exist in the corresponding sidecar file, then that field will be created according to the ``entries``. If a field does exist in the corresponding sidecar file, then that field will be updated according to the ``entries``. For example, if ``InstitutionName`` is not defined in the sidecar json file, then trying to update ``InstitutionName`` to ``Martinos Center`` will update the sidecar json file to have ``InstitutionName`` as ``Martinos Center``. Parameters ---------- bids_path : BIDSPath The set of paths to update. The :class:`mne_bids.BIDSPath` instance passed here **must** have the ``.root`` attribute set. The ``.datatype`` attribute **may** be set. If ``.datatype`` is not set and only one data type (e.g., only EEG or MEG data) is present in the dataset, it will be selected automatically. This must uniquely identify an existing file path, else an error will be raised. entries : dict | str | pathlib.Path A dictionary, or JSON file that defines the sidecar fields and corresponding values to be updated to. %(verbose)s Notes ----- This function can only update JSON files. Sidecar JSON files include files such as ``*_ieeg.json``, ``*_coordsystem.json``, ``*_scans.json``, etc. You should double check that your update dictionary is correct for the corresponding sidecar JSON file because it will perform a dictionary update of the sidecar fields according to the passed in dictionary overwriting any information that was previously there. Raises ------ RuntimeError If the specified ``bids_path.fpath`` cannot be found in the dataset. RuntimeError If the ``bids_path.fpath`` does not have ``.json`` extension. Examples -------- Update a sidecar JSON file >>> from pathlib import Path >>> root = Path('./mne_bids/tests/data/tiny_bids').absolute() >>> bids_path = BIDSPath(subject='01', task='rest', session='eeg', ... suffix='eeg', extension='.json', datatype='eeg', ... root=root) >>> entries = {'PowerLineFrequency': 60} >>> update_sidecar_json(bids_path, entries, verbose=False) """# get all matching json filesbids_path=bids_path.copy()ifbids_path.extension!=".json":raiseRuntimeError('Only works for ".json" files. The '"BIDSPath object passed in has "f"{bids_path.extension} extension.")# get the file pathfpath=bids_path.fpathifnotfpath.exists():raiseRuntimeError(f"Sidecar file does not "f"exist for {fpath}.")# sidecar update either from file, or as dictionaryifisinstance(entries,dict):sidecar_tmp=entrieselse:withopen(entries)astmp_f:sidecar_tmp=json.load(tmp_f,object_pairs_hook=OrderedDict)logger.debug(sidecar_tmp)logger.debug(f"Updating {fpath}...")# load in sidecar filepathwithopen(fpath)astmp_f:sidecar_json=json.load(tmp_f,object_pairs_hook=OrderedDict)# update sidecar JSON file with the fields passed insidecar_json.update(**sidecar_tmp)# write back the sidecar JSON_write_json(fpath,sidecar_json,overwrite=True)
def_update_sidecar(sidecar_fname,key,val):"""Update a sidecar JSON file with a given key/value pair. Parameters ---------- sidecar_fname : str | os.PathLike Full name of the data file key : str The key in the sidecar JSON file. E.g. "PowerLineFrequency" val : str The corresponding value to change to in the sidecar JSON file. """withopen(sidecar_fname,encoding="utf-8-sig")asfin:sidecar_json=json.load(fin)sidecar_json[key]=val_write_json(sidecar_fname,sidecar_json,overwrite=True)
[docs]@verbosedefupdate_anat_landmarks(bids_path,landmarks,*,fs_subject=None,fs_subjects_dir=None,kind=None,on_missing="raise",verbose=None,):"""Update the anatomical landmark coordinates of an MRI scan. This will change the ``AnatomicalLandmarkCoordinates`` entry in the respective JSON sidecar file, or create it if it doesn't exist. Parameters ---------- bids_path : BIDSPath Path of the MR image. landmarks : mne.channels.DigMontage | path-like An :class:`mne.channels.DigMontage` instance with coordinates for the nasion and left and right pre-auricular points in MRI voxel coordinates. Alternatively, the path to a ``*-fiducials.fif`` file as produced by the MNE-Python coregistration GUI or via :func:`mne.io.write_fiducials`. .. note:: :func:`mne_bids.get_anat_landmarks` provides a convenient and reliable way to generate the landmark coordinates in the required coordinate system. .. note:: If ``path-like``, ``fs_subject`` and ``fs_subjects_dir`` must be provided as well. .. versionchanged:: 0.10 Added support for ``path-like`` input. fs_subject : str | None The subject identifier used for FreeSurfer. Must be provided if ``landmarks`` is ``path-like``; otherwise, it will be ignored. fs_subjects_dir : path-like | None The FreeSurfer subjects directory. If ``None``, defaults to the ``SUBJECTS_DIR`` environment variable. Must be provided if ``landmarks`` is ``path-like``; otherwise, it will be ignored. 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 on_missing : 'ignore' | 'warn' | 'raise' How to behave if the specified landmarks cannot be found in the MRI JSON sidecar file. .. versionadded:: 0.10 %(verbose)s Notes ----- .. versionadded:: 0.8 """_validate_type(item=bids_path,types=BIDSPath,item_name="bids_path")_validate_type(item=landmarks,types=(DigMontage,"path-like"),item_name="landmarks")_check_on_missing(on_missing)# Do some path verifications and fill in some gaps the users might have# left (datatype and extension)# XXX We could be more stringent (and less user-friendly) and insist on a# XXX full specification of all parts of the BIDSPath, thoughts?bids_path_mri=bids_path.copy()ifbids_path_mri.datatypeisNone:bids_path_mri.datatype="anat"ifbids_path_mri.datatype!="anat":raiseValueError(f'Can only operate on "anat" MRI data, but the provided bids_path 'f"points to: {bids_path_mri.datatype}")ifbids_path_mri.suffixisNone:raiseValueError('Please specify the "suffix" entity of the provided '"bids_path.")elifbids_path_mri.suffixnotin("T1w","FLASH"):raiseValueError(f'Can only operate on "T1w" and "FLASH" images, but the bids_path 'f"suffix indicates: {bids_path_mri.suffix}")valid_extensions=(".nii",".nii.gz")tried_paths=[]file_exists=Falseifbids_path_mri.extensionisNone:# No extension was provided, start searching …forextensioninvalid_extensions:bids_path_mri.extension=extensiontried_paths.append(bids_path_mri.fpath)ifbids_path_mri.fpath.exists():file_exists=Truebreakelse:# An extension was providedtried_paths.append(bids_path_mri.fpath)ifbids_path_mri.fpath.exists():file_exists=Trueifnotfile_exists:raiseValueError(f"Could not find an MRI scan. Please check the provided "f"bids_path. Tried the following filenames: "f'{", ".join([p.nameforpintried_paths])}')ifnotisinstance(landmarks,DigMontage):# it's pathlikeiffs_subjectisNone:raiseValueError('You must provide the "fs_subject" parameter when passing the '"path to fiducials")landmarks=_get_landmarks_from_fiducials_file(bids_path=bids_path,fname=landmarks,fs_subject=fs_subject,fs_subjects_dir=fs_subjects_dir,)positions=landmarks.get_positions()coord_frame=positions["coord_frame"]ifcoord_frame!="mri_voxel":raiseValueError(f"The landmarks must be specified in MRI voxel coordinates, but "f'provided DigMontage is in "{coord_frame}"')# Extract the cardinal pointsname_to_coords_map={"LPA":positions["lpa"],"NAS":positions["nasion"],"RPA":positions["rpa"],}# Check if coordinates for any cardinal point are missing, and convert to# a list so we can easily store the data in JSON formatmissing_points=[]forname,coordsinname_to_coords_map.items():ifcoordsisNone:missing_points.append(name)else:# Funnily, np.float64 is JSON-serializabe, while np.float32 is not!# Thus, cast to float64 to avoid issues (which e.g. may arise when# fiducials were read from disk!)name_to_coords_map[name]=list(coords.astype("float64"))ifmissing_points:raiseValueError(f"The provided DigMontage did not contain all required cardinal "f"points (nasion and left and right pre-auricular points). The "f"following points are missing: "f'{", ".join(missing_points)}')bids_path_json=bids_path.copy().update(extension=".json")ifnotbids_path_json.fpath.exists():# Must exist before we can update it_write_json(bids_path_json.fpath,dict())mri_json=json.loads(bids_path_json.fpath.read_text(encoding="utf-8"))if"AnatomicalLandmarkCoordinates"notinmri_json:_on_missing(on_missing=on_missing,msg=f"No AnatomicalLandmarkCoordinates section found in "f"{bids_path_json.fpath.name}",error_klass=KeyError,)mri_json["AnatomicalLandmarkCoordinates"]=dict()forname,coordsinname_to_coords_map.items():ifkindisnotNone:name=f"{name}_{kind}"ifnamenotinmri_json["AnatomicalLandmarkCoordinates"]:_on_missing(on_missing=on_missing,msg=f"Anatomical landmark not found in "f"{bids_path_json.fpath.name}: {name}",error_klass=KeyError,)mri_json["AnatomicalLandmarkCoordinates"][name]=coordsupdate_sidecar_json(bids_path=bids_path_json,entries=mri_json)
def_get_landmarks_from_fiducials_file(*,bids_path,fname,fs_subject,fs_subjects_dir):"""Get anatomical landmarks from fiducials file, in MRI voxel space."""# avoid dicrular importsfrommne_bids.writeimport(_get_fid_coords,_get_t1w_mgh,_mri_landmarks_to_mri_voxels,)digpoints,coord_frame=read_fiducials(fname)# All of this should be guaranteed, but better be safe than sorry!assertcoord_frame==FIFF.FIFFV_COORD_MRIassertdigpoints[0]["ident"]==FIFF.FIFFV_POINT_LPAassertdigpoints[1]["ident"]==FIFF.FIFFV_POINT_NASIONassertdigpoints[2]["ident"]==FIFF.FIFFV_POINT_RPAmontage_loaded=make_dig_montage(lpa=digpoints[0]["r"],nasion=digpoints[1]["r"],rpa=digpoints[2]["r"],coord_frame="mri",)landmark_coords_mri,_=_get_fid_coords(dig_points=montage_loaded.dig)landmark_coords_mri=np.asarray((landmark_coords_mri["lpa"],landmark_coords_mri["nasion"],landmark_coords_mri["rpa"],))t1w_mgh=_get_t1w_mgh(fs_subject,fs_subjects_dir)landmark_coords_voxels=_mri_landmarks_to_mri_voxels(mri_landmarks=landmark_coords_mri*1000,t1_mgh=t1w_mgh,# in mm)montage_voxels=make_dig_montage(lpa=landmark_coords_voxels[0],nasion=landmark_coords_voxels[1],rpa=landmark_coords_voxels[2],coord_frame="mri_voxel",)returnmontage_voxels