diff --git a/example1/resources/Appariement.xlsx b/example1/resources/Appariement.xlsx index 29036a340230af04b8190f9691673a48c4f9b02d..1417168e898a376032013809371d3dee434519f8 100644 Binary files a/example1/resources/Appariement.xlsx and b/example1/resources/Appariement.xlsx differ diff --git a/example1/resources/README.md b/example1/resources/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7de8b90af73eb18195feb6b674583b69edb8d6ab --- /dev/null +++ b/example1/resources/README.md @@ -0,0 +1,327 @@ +# Bidscoin Example 1 + +## Introduction + +This dataset is an purely fictional, designed to demonstrate the core +features of `bidscoin` bidsifier tool. + +The structure of dataset is modelled of real-life dataset, currently unpublished. +Several simplifications has been applied, conserving only the MRI images structure, +general participants book-keeping, naming schema and few auxiliary files. + +MRI recordings are stored in Nifti format with an additional json file containing +the dump of Dicom header, created by [hmri toolbox](https://hmri-group.github.io/hMRI-toolbox/) +for [SPM12](https://www.fil.ion.ucl.ac.uk/spm/software/spm12/). +All `.nii` images are replaced by an empty file, and any personal information is removed +from json files. + +## Experiment description + +The experiment is designed to study of effect of fatigue on memory performance. + +5 participants are separated into pairs with matched sex, age and years of education. +First persons of pairs are used for study (patient group), while paired persons are used for control. + +During experiment, each participant is scanned 3 times (sessions), for each of session they are asked to perform either a memory or a stroop task: + +- **HLC** with memory task performed after a tiring task (High Cognitive Load) + - In additional to functional and structural, a diffusion scan is present +- **LCL** with memory task performed without tiring task (Low Cognitive Load) + - Session contains structural and functional MRI scans +- **STROOP** with a standard stroop task + - session contains only multi parametric mapping MRI (MPM) + +The order in which each scan is performed may vary from participant to participant. + +## Original dataset structure + +The original data is stored in `source` directory. Data corresponding to each participants +is stored in `source/<participant id>` sub-folder, where `<participant id>` the code of +participant padded with `0`. + +Inside participants sub-folders, 3 folders of session data is places. The folder names +don't have a direct correspondence with session, bit represent a code applied by a scanner, +in form `sXYZ`. + +The image data is stored directly in session sub-folder `nii`. +For **LCL** and **HCL** sessions, task and assessment are stored in `inp` sub-folder. + +Tiring task, and stroop task data are not present in dataset. + +### Memory task description + +Task consist of a classic n-back working memory update task. +A set of letters is presented to participant. Each letter is presented during `1.7s`, +followed by `0.5s` fixation cross presentation. Participant is asked to remember +if such letter was present in the last, 2 cards ago or 3 cards age (1back, 2back, 3back). +A participant response ("c" for correct, "n" for non-correct) is registered alongside with +expected response. +A fill task consists of 18 blocks of 1,2,3-back tasks, with 16 presented letters in each block. + +Task results are formatted following [bids](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/05-task-events.html), +and stored in `source/<subject>/<session>/nii/FCsepNBack.tsv` file. + +### Assesment description + +Each task is followed by visual analogue assessment (VAS) questioner, where participant is +asked to estimate his psychological state from bad (0) to good (100). +In particular the next estimations are requested: + +- **Motivation** +- **Hapiness** +- **Fatigue** +- **Openness** +- **Stress** +- **Anxiety** +- **Effort** + +The results are formatted following [bids](https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#phenotypic-and-assessment-data), +and stored in `source/<subject>/<session>/nii/VAS.tsv` + +### MRI scanning sessions + +#### LCL + +During the LCL session, the next acquisitions are taken: + +- localisation (protocol: `localizer`) +- a short fMRI sequence with inverted phase-encoding direction +(protocol: `cmrr_mbep2d_bold_mb2_invertpe`) +- a fMRI sequence during nBack task execution (protocol: `cmrr_mbep2d_bold_mb2_task_nfat`) +- a short fMRI sequence with inverted phase-encoding direction +(protocol: `cmrr_mbep2d_bold_mb2_invertpe`) +- a fMRI sequence without task execution -- in resting state (protocol: `cmrr_mbep2d_bold_mb2_rest`) +- a magnitude encoded fieldmap sequence (protocol: `gre_field_mapping`) +- a phase-difference fieldmap sequence (protocol: `gre_field_mapping`) +- a FLAIR sequence (protocol: `t1_mpr_sag_p2_iso`) +- a T2 weighted sequence (protocol: `t2_spc_da-fl_sag_p2_iso`) + +#### HCL + +During HCL session, the next acquisitions are taken: + +- localisation (protocol: `localizer`) +- a short fMRI sequence with inverted phase-encoding direction +(protocol: `cmrr_mbep2d_bold_mb2_invertpe`) +- a fMRI sequence during nBack task execution (protocol: `cmrr_mbep2d_bold_mb2_task_fat`) +- a short fMRI sequence with inverted phase-encoding direction +(protocol: `cmrr_mbep2d_bold_mb2_invertpe`) +- a fMRI sequence without task execution -- in resting state (protocol: `cmrr_mbep2d_bold_mb2_rest`) +- a magnitude encoded fieldmap sequence (protocol: `gre_field_mapping`) +- a phase-difference fieldmap sequence (protocol: `gre_field_mapping`) +- a diffusion sequence with inverted gradient direction (protocol: `cmrr_mbep2d_diff_NODDI_invertpe`) +- a diffusion sequence with normal gradient direction (protocol: `cmrr_mbep2d_diff_NODDI`) +- a diffusion sequence without RF-pulse (protocol: `cmrr_mbep2d_diff_NODDI_noise`) + + +#### STROOP + +During STROOP session, the next acquisitions are taken: + +- localisation (protocol: `localizer`) +- head-localised fieldmap for PD weighted sMRI (protocol: `al_mtflash3d_sensArray`) +- body-localised fieldmap for PD weighted sMRI (protocol: `al_mtflash3d_sensBody`) +- magnitude-encoded PD weighted structural MRI (protocol: `al_mtflash3d_PDw`) +- phase-encoded PD weighted structural MRI (protocol: `al_mtflash3d_PDw`) +- head-localised fieldmap for T1 weighted sMRI (protocol: `al_mtflash3d_sensArray`) +- body-localised fieldmap for T1 weighted sMRI (protocol: `al_mtflash3d_sensBody`) +- magnitude-encoded T1 weighted structural MRI (protocol: `al_mtflash3d_T1w`) +- phase-encoded T1 weighted structural MRI (protocol: `al_mtflash3d_T1w`) +- head-localised fieldmap for PD weighted sMRI (protocol: `al_mtflash3d_sensArray`) +- body-localised fieldmap for PD weighted sMRI (protocol: `al_mtflash3d_sensBody`) +- magnitude-encoded MT weighted structural MRI (protocol: `al_mtflash3d_MTw`) +- phase-encoded MT weighted structural MRI (protocol: `al_mtflash3d_MTw`) +- a B1 mapping with RF flip-angle relaxation (protocol: `al_B1mapping`) +- a magnitude encoded fieldmap sequence (protocol: `gre_field_mapping`) +- a phase-difference fieldmap sequence (protocol: `gre_field_mapping`) + +### Additional files + +All non-data files corresponding to dataset are stored in `resources` subfolder + +#### Participants bookkeeping `Appariement.xlsx` + +`Appariement.xlsx` is an excel table containing the list of participants with key +demographic data. + +Columns are, in order: + +- **Patient**: Id of participant, padded with `0` +- **Sex**: Sex of participant, either `M` (male) or `F` (female) +- **Age**: Age of participant, in years +- **Education**: Years of education +- **1**: Name of the first scanned session (session *OUT* signify dropped-out participant) +- **2**: Name of the second scanned session +- **3**: Name of the third scanned session +- **Control**: Id of paired participant, padded with `0` +- **Sex**: Sex of paired participant, either `M` (male) or `F` (female) +- **Age**: Age of paired participant, in years +- **Education**: Years of education +- **1**: Name of the first scanned session (session *OUT* signify dropped-out participant) +- **2**: Name of the second scanned session +- **3**: Name of the third scanned session + +#### Sidecar json files + +Prepeared json files to use as [descriptions](https://bids-specification.readthedocs.io/en/stable/02-common-principles.html#tabular-files) +for bidsified `.tsv` files: + +- `participants.json` is a sidecar json file for `participant.tsv` file, containing list +of participants together with demographic information + - alternative files `participants_add.json` and `participants_remove.json` are used for +demonstration of participant table manipulations by `bidscoin` +- `FCsepNBack.json` is sidecar json file for task table +- `VAS.json` is sidecar json file for VAS + + +#### bval and bvec files + +`bval` and `bvec` files used to accompany [diffusion data](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#diffusion-imaging-data) +are placed in `resources/diffusion` folder. They are common to all diffusion images used +in this dataset. + +#### Bidsmap files + +Generated bidsmap files, that can be used to bidsify this dataset are placed in `resources/map` directory: + +- `bidsmap.yaml` must be used together with plugins +- `bidsmap_noPlugin.yaml` can be used without plugins + +These files can be used with `-b` option directly, or copied into `bids/code/bidscoin` directory. + +#### Plugins + +The plugins are stored in `resources/plugins` directory, and contains commented example of additional data management provided by `bidscoin` infrastructure. + +- `definitions.py` contains some common functions used by plugin and list of sessions and protocols used to check dataset validity +- `rename_plugin.py` retrieves the demographic data and sessions names from `Appariement.xlsx`bookkeeping file +- `process_plugin.py` contains some example of intermediate data processing, namely merging functional and diffusion 3D images into 4D images, it also shows example of subject demographic data modification +- `bidsify_plugin.py` contains examples of recording metadata modification in order to facilitate recordings identification + +#### Dataset description files +[The dataset description](https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#dataset-description) +consists of two files: + +- `dataset_description.json`, a minimal example of json file describing dataset +- `README.md`, this file + + +## How to run example + +Dataset bidsification is composed of two steps: data preparation and data bidsification. +An optional data-processing step can be inserted between preparation and bidsification. + +A one-time step of bidsmap creation may be necessary. + +### Data preparation + +In this step, a generic user-defined dataset is organized in a standardized way. + +To run data preparation, it will be enough to run from `example1` directory + +``` +python3 bidscoin.py prepare --part-template resources/participants.json --recfolder nii=MRI --plugin resources/plugins/rename_plugin.py source/ renamed/ +``` + +The options `--part-template resources/participants.json` will tell bidscoin to use participant json file as template for `participants.tsv` file. +The column `participant_id` will be filled automatically, while other columns will be filled +by default by `n/a`, unless they are set in plugin: + +``` +session.sub_values["sex"] = "M" +``` + +Without `--part-template` option the only column in participants file will be `participant_id`. + +Option `--recfolder nii=MRI` will tell to `bidscoin` that image files are MRI and stored in `nii` folder. +Without this option `bidscoin` will be unable to find image files. + +Option `--plugin resources/plugins/rename-plugin.py` will tell to bidscoin to load corresponding plugin. + +Parameters `source/` and `renamed/` tells to bidscoin where to search for source dataset and where place prepared dataset. + +After the execution of preparation, the `rename` folder should contain folders and files: + +- **code/bidscoin**, with log files of the last execution of preparation step +- **participants.tsv** and **participants.json** files with formatted and filled participant list, all columns for all subjects must be filled except `handiness`, which should contain only `n/a` +- **sub-00X** folders for subjects 1-4 + - **ses-HCL** sub-folders with bidsified session name (either `ses-LCL`, if run with plugin, of `ses-s01905` if run without plugin) + - **auxiliary** folder with task and VAS tables and json (only if run with plugin) + - **MRI** subfolder containing MRI data + - **00x-<seq_name>** folders with original image data organased by sequences + +This is prepared dataset, and can be modified freely at condition to conserve general structure. +For example the participant table can be corrected if contain wrong or missing values. + +Running bidscoin with all options can be tedious. To streamline the experience, the majority of options can be saved in configuration file by running + +``` +python3 bidscoin.py -c conf.yamel --conf-save prepare <options> source/ renamed/ +``` + +This will create a local `conf.yamel` file with passed options. +To load the configuration: + +``` +python3 bidscoin.py -c conf.yamel prepare source/ renamed/ +``` + +Passing other options and using switch `--conf-save` will update configuration file. + + +### Bidsmap creation + +Bidsmap is created/tested with `map` command: + +``` +python3 bidscoin.py map --plugin resources/plugins/bidsify_plugin.py --template bidsmap_template.yaml renamed/ bids/ +``` + +The option `--plugin resources/plugins/bidsify_plugin.py` will load correspondent plugin (the used plugin is the same as for bidsification to ensure that all modifications needed to +identify scans are applied). + +The option `--template bidsmap_template.yaml` tells which template will be used. The template +reads the common metatdata and tries to guess the modality. This is based on protocol names and can vary from institute to institute. +The `bidsmap_template.yaml` works with example dataset, but for real data a different template may be needed. + +The parameters `renamed/` and `bids/` tells where prepared dataset is stored and where the bidsified dataset will be placed. + +First execution of `map` usually results into huge amount of warnings and occasional errors. +These warnings and errors must be corrected. The details of various warnings and corrections to apply can be found in `bidscoin` documentation. + +The working bidsmap can be found in `resources/map` directory. +If placed in `bids/code/bidscoin/` directory, the `map` should not produce any warnings. + + + +### Process step + +The process step is an optional step, which allow limited data manipulation before bidsification. +Without plugins, it just verifies that all data is identifiable, and files with same bids name +do not exists in bids dataset. +So it can be used as check before bidsification. + +With plugins, it can be used for data manipulation, and metadata completion. +For example `resources/plugins/process_plugin.py` fills the `nandiness` column, and merges +fMRI and diffusion images in single 4D image. + +``` +python3 bidscoin.py process --plugin resources/plugins/process_plugin.py renamed/ bids/ +``` + +After running, the column `handiness` must be filled and fMRI files +(for ex. in `renamed/sub-002/ses-LCL/MRI/004-cmrr_mbep2d_bold_mb2_task_nfat/`) +must be merged in one file. + +This step can be easily replaced by any custom script and/or pipeline. The only advantage +is some `bids` and `bidscoin` specific checks and recording identification. + + +### Bidsification step + +The final step is bidsification, it is run with `bidsify` command: + +``` +python3 bidscoin.py map --plugin resources/plugins/bidsify_plugin.py renamed/ bids/ +``` + diff --git a/example1/resources/dataset_description.json b/example1/resources/dataset_description.json new file mode 100644 index 0000000000000000000000000000000000000000..5a60c8c7c98f78e42a32becef42e850b7f7de5be --- /dev/null +++ b/example1/resources/dataset_description.json @@ -0,0 +1,5 @@ +{ + "Name": "Bidscoin Example 1", + "BIDSVersion": "1.2.0", + "License": "PD" +} diff --git a/example1/resources/map/bidsmap.yaml b/example1/resources/map/bidsmap.yaml index 7171f5f9407415f9f8869135fc692147c4bf5b67..89395f4c9a4810b1929afec7e24b80ad8a69fdb0 100644 --- a/example1/resources/map/bidsmap.yaml +++ b/example1/resources/map/bidsmap.yaml @@ -1,5 +1,5 @@ Options: - version: 2.3.1 + version: 2.3.0 bidsignore: [] PlugIns: path: resources/plugins/bidsify_plugin.py diff --git a/example1/resources/map/bidsmap_noPlugin.yaml b/example1/resources/map/bidsmap_noPlugin.yaml index 2bf758fca152f6dd45c113009fe6609663eba1d8..af46be7b663a6c36e926cc76956908382a44b452 100644 --- a/example1/resources/map/bidsmap_noPlugin.yaml +++ b/example1/resources/map/bidsmap_noPlugin.yaml @@ -1,5 +1,5 @@ Options: - version: 2.3.1 + version: 2.3.o bidsignore: [] PlugIns: path: ~ diff --git a/example1/resources/plugins/bidsify_plugin.py b/example1/resources/plugins/bidsify_plugin.py index 6703e2be5f2e30af1941b1d18fc52515ecf4cf42..916ec57db0ae7c4290c9a11568e95877b7b5623f 100644 --- a/example1/resources/plugins/bidsify_plugin.py +++ b/example1/resources/plugins/bidsify_plugin.py @@ -5,19 +5,61 @@ import random from definitions import checkSeries + +# defining logger this way will prefix plugin messages +# with plugin name logger = logging.getLogger(__name__) -# global variables -rawfolder = "" -bidsfolder = "" +############################# +# global bidscoin variables # +############################# + +# Folder with prepared dataset +preparedfolder = None +# folder with bidsified dataset +bidsfolder = None +# switch if is a dry-run (test run) dry_run = False -participants_table = None -rec_path = "" -countSeries = {} + +##################### +# Session variables # +##################### + +# Some sequences within session (namely fMRI and MPM structural) follows same +# protocol, thus it is impossible to identify them only using +# metadata +# we will identify them by order they appear in session + +# list of sequences in order of acquisition in current session +seq_list = list() + + +##################### +# Sequence variable # +##################### + +# The index of current sequence, corresponds to order in the sequence list +seq_index = -1 + +# Identified tag for fMRI and MPM MRI +# This tag will override "SeriesDescription" DICOM tag +IntendedFor = "" def InitEP(source: str, destination: str, dry: bool) -> int: + """ + Initialisation of plugin + + 1. Saves source/destination folders and dry_run switch + + Parameters + ---------- + source: str + path to source dataset + destination: + path to prepared dataset + """ global rawfolder global bidsfolder global dry_run @@ -28,31 +70,81 @@ def InitEP(source: str, destination: str, dry: bool) -> int: def SubjectEP(scan): + """ + Subject modification + """ + + #################### + # Subject renaming # + #################### + + # This will demonstrate the subject renaming + # namely increasing the id by 1 sub_id = int(scan.subject[4:]) scan.subject = "sub-{:03d}".format(sub_id + 1) + # changing also in participant.tsv file + if scan.sub_values["paired"]: + pair_id = int(scan.sub_values["paired"][4:]) + scan.sub_values["paired"] = "sub-{:03d}".format(pair_id + 1) + + ################################# + # Subject metadata manipulation # + ################################# + + # these modifications will appear only if corresponding + # columns are declared in participants.json + # they will not allow to add/remove columns + + # to modify the columns, use --part-template cli option + + # this will remove information on sex of subject, from bidsified dataset, + # but not corresponding columns scan.sub_values["sex"] = None + + # this will fill new column "random" + # if this column is in participant.json, it will be shown + # in bidsified participant.tsv scan.sub_values["random"] = random.random() def SessionEP(scan): - global series - global sid - sub = scan.subject - ses = scan.session - # path = os.path.join(rawfolder, - # sub, ses, - # "MRI") + """ + Session files modification + + 1. Stores the list of sequences in session + 2. Checks the sequences + 3. Copies HCL and LCL task and KSS/VAS files + to bidsified dataset + """ + + ###################################### + # Initialisation of sesion variables # + ###################################### + # retrieving list of sequences and puttintg them into list + global seq_list + global seq_index path = os.path.join(scan.in_path, "MRI") - series = sorted(os.listdir(path)) - series = [s.split("-", 1)[1] for s in series] - sid = -1 - checkSeries(path, sub, ses, False) - # copytng behevioral data + seq_list = sorted(os.listdir(path)) + seq_list = [s.split("-", 1)[1] for s in seq_list] + seq_index = -1 + + ################################# + # Checking sequences in session # + ################################# + checkSeries(path, scan.subject, scan.session, False) + + ############################################# + # Checking for existance of auxiliary files # + ############################################# + + # all the copy instructions must be protected by + # if not dry_run + aux_input = os.path.join(scan.in_path, "auxiliary") - if ses in ("ses-LCL", "ses-HCL"): + if scan.session in ("ses-LCL", "ses-HCL"): if not os.path.isdir(aux_input): logger.error("Session {}/{} do not contain auxiliary folder" - .format(sub, ses)) + .format(scan.subject, scan.session)) raise FileNotFoundError("folder {} not found" .format(aux_input)) beh = os.path.join(scan.in_path, "beh") @@ -63,88 +155,96 @@ def SessionEP(scan): ("VAS.tsv", "task-rest_beh.tsv"), ("VAS.json", "task-rest_beh.json")): source = "{}/{}".format(aux_input, old) - dest = "{}/{}_{}_{}".format(beh, sub, ses, new) + dest = "{}/{}_{}_{}".format(beh, scan.subject, scan.session, new) if not os.path.isfile(source): if dry_run: logger.error("{}/{}: File {} not found" - .format(sub, ses, source)) + .format(scan.subject, scan.session, source)) else: logger.critical("{}/{}: File {} not found" - .format(sub, ses, source)) + .format(scan.subject, + scan.session, + source)) raise FileNotFoundError(source) if os.path.isfile(dest): logger.warning("{}/{}: File {} already exists" - .format(sub, ses, dest)) + .format(scan.subject, scan.session, dest)) if not dry_run: shutil.copy2(source, dest) -series = list() -sid = -1 -Intended = "" - - def SequenceEP(recording): - global series - global sid - global Intended - Intended = "" - sid += 1 - recid = series[sid] + """ + Sequence identification + """ + global seq_index + global IntendedFor + IntendedFor = "" + seq_index += 1 + recid = seq_list[seq_index] + + # checking if current sequence corresponds in correct place in list if recid != recording.recId(): logger.warning("{}: Id mismatch folder {}" .format(recording.recIdentity(False), recid)) + # The inverted fMRI are taken just before normal fMRI + # looking into the following sequence will identify + # the current one if recid == "cmrr_mbep2d_bold_mb2_invertpe": - mod = series[sid + 1] + mod = seq_list[seq_index + 1] if mod.endswith("cmrr_mbep2d_bold_mb2_task_fat"): - Intended = "nBack" + IntendedFor = "nBack" elif mod.endswith("cmrr_mbep2d_bold_mb2_task_nfat"): - Intended = "nBack" + IntendedFor = "nBack" elif mod.endswith("cmrr_mbep2d_bold_mb2_rest"): - Intended = "rest" + IntendedFor = "rest" else: - Intended = "invalid" + IntendedFor = "invalid" logger.warning("{}: Unknown session {}" .format(recording.recIdentity(), mod)) + # fmap images are taken for HCL, LCL and MPM (STROOP) + # sessions elif recid == "gre_field_mapping": if recording.sesId() in ("ses-HCL", "ses-LCL"): - Intended = "HCL/LCL" + IntendedFor = "HCL/LCL" elif recording.sesId() == "ses-STROOP": - Intended = "STROOP" + IntendedFor = "STROOP" else: logger.warning("{}: Unknown session {}" .format(recording.recIdentity(), recording.sesId())) - Intended = "invalid" + IntendedFor = "invalid" + # fmaps sesnsBody and sesnArray are taken just before + # structural PD , T1 and MT. Looking into next sequences + # will allow the identification elif recid == "al_mtflash3d_sensArray": - det = series[sid + 2] + det = seq_list[seq_index + 2] if det.endswith("al_mtflash3d_PDw"): - Intended = "PDw" + IntendedFor = "PDw" elif det.endswith("al_mtflash3d_T1w"): - Intended = "T1w" - recording.setAttribute("Intended", "T1w") + IntendedFor = "T1w" elif det.endswith("al_mtflash3d_MTw"): - Intended = "MTw" + IntendedFor = "MTw" else: logger.warning("{}: Unable determine modality" .format(recording.recIdentity())) - Intended = "invalid" + IntendedFor = "invalid" elif recid == "al_mtflash3d_sensBody": - det = series[sid + 1] + det = seq_list[seq_index + 1] if det.endswith("al_mtflash3d_PDw"): - Intended = "PDw" + IntendedFor = "PDw" elif det.endswith("al_mtflash3d_T1w"): - Intended = "T1w" + IntendedFor = "T1w" elif det.endswith("al_mtflash3d_MTw"): - Intended = "MTw" + IntendedFor = "MTw" else: logger.warning("{}: Unable determine modality" .format(recording.recIdentity())) - Intended = "invalid" + IntendedFor = "invalid" def RecordingEP(recording): - if Intended != "": - recording.setAttribute("SeriesDescription", Intended) + if IntendedFor != "": + recording.setAttribute("SeriesDescription", IntendedFor) diff --git a/example1/resources/plugins/definitions.py b/example1/resources/plugins/definitions.py index 7b30193ed84785ac7fe46965a59801ec641e0e7c..ba0e37b4386fdeb61f4521b9e123afb055df45cc 100644 --- a/example1/resources/plugins/definitions.py +++ b/example1/resources/plugins/definitions.py @@ -6,7 +6,7 @@ import os # and appear with this file-name logger = logging.getLogger(__name__) -# path to the root folder of plugin +# path to the root folder of plugin # (bidscoin_example/example1/resources) # usefull to retrieve auxiliary files plugin_root = os.path.normpath( @@ -17,9 +17,9 @@ plugin_root = os.path.normpath( # of scans Series = { "ses-LCL": ('localizer', - 'cmrr_mbep2d_bold_mb2_invertpe', - 'cmrr_mbep2d_bold_mb2_task_nfat', - 'cmrr_mbep2d_bold_mb2_invertpe', + 'cmrr_mbep2d_bold_mb2_invertpe', + 'cmrr_mbep2d_bold_mb2_task_nfat', + 'cmrr_mbep2d_bold_mb2_invertpe', 'cmrr_mbep2d_bold_mb2_rest', 'gre_field_mapping', 'gre_field_mapping', @@ -68,7 +68,7 @@ def checkSeries(path: str, subject: str, session: str, critical: bool) -> bool: """ - Retrieve list of series from path and checks + Retrieve list of series from path and checks its compatibility with defined list Parameters: @@ -80,7 +80,7 @@ def checkSeries(path: str, session: str Name of session to check critical: bool - If True, mismatches will creeate exceptions + If True, mismatches will creeate exceptions and critical level log entries """ if session not in Series: @@ -89,7 +89,7 @@ def checkSeries(path: str, return False passed = True series = sorted(os.listdir(path)) - series = [s.split("-",1)[1] for s in series] + series = [s.split("-", 1)[1] for s in series] for ind, s in enumerate(series): if s not in Series[session]: msg = "{}/{}: Invalid serie {}".format(subject, session, s) @@ -139,9 +139,9 @@ def checkSeries(path: str, return passed -def reportError(msg: str, critical: bool, error: type=ValueError) -> None: +def reportError(msg: str, critical: bool, error: type = ValueError) -> None: """ - reports error. + reports error. If critical, an exception of type error will raise Parametres: @@ -155,6 +155,6 @@ def reportError(msg: str, critical: bool, error: type=ValueError) -> None: """ if critical: logger.critical(msg) - raise exception(msg) + raise Exception(msg) else: logger.error(msg) diff --git a/example1/resources/plugins/process_plugin.py b/example1/resources/plugins/process_plugin.py index ec84388edaac840a4e6e0647d3a70010e4b9f529..f8a42f53e1b07e2be4ab2ae9f643e45e9e76bbe3 100644 --- a/example1/resources/plugins/process_plugin.py +++ b/example1/resources/plugins/process_plugin.py @@ -5,6 +5,9 @@ import random from definitions import checkSeries, plugin_root + +# defining logger this way will prefix plugin messages +# with plugin name logger = logging.getLogger(__name__) ############################# @@ -23,19 +26,25 @@ dry_run = False # Session variables # ##################### -# list of sequences in current session -# used to identify fMRI and MPM MRI images -series = list() +# Some sequences within session (namely fMRI and MPM structural) follows same +# protocol, thus it is impossible to identify them only using +# metadata +# we will identify them by order they appear in session + +# list of sequences in order of acquisition in current session +seq_list = list() + ##################### # Sequence variable # ##################### -# The id of current sequence -sid = -1 +# The index of current sequence, corresponds to order in the sequence list +seq_index = -1 # Identified tag for fMRI and MPM MRI -Intended = "" +# This tag will override "SeriesDescription" DICOM tag +IntendedFor = "" def InitEP(source: str, destination: str, dry: bool) -> int: @@ -87,25 +96,27 @@ def SessionEP(scan): ###################################### # Initialisation of sesion variables # ###################################### - global series - global sid - sub = scan.subject - ses = scan.session + # retrieving list of sequences and puttintg them into list + global seq_list + global seq_index path = os.path.join(scan.in_path, "MRI") - series = sorted(os.listdir(path)) - series = [s.split("-", 1)[1] for s in series] - sid = -1 - checkSeries(path, sub, ses, False) + seq_list = sorted(os.listdir(path)) + seq_list = [s.split("-", 1)[1] for s in seq_list] + seq_index = -1 + ################################# + # Checking sequences in session # + ################################# + checkSeries(path, scan.subject, scan.session, False) ############################################# # Checking for existance of auxiliary files # ############################################# - aux_input = os.path.join(session.in_path, "auxiliary") - if ses in ("ses-LCL", "ses-HCL"): + aux_input = os.path.join(scan.in_path, "auxiliary") + if scan.session in ("ses-LCL", "ses-HCL"): if not os.path.isdir(aux_input): logger.error("Session {}/{} do not contain auxiliary folder" - .format(sub, ses)) + .format(scan.subject, scan.session)) return -1 for old, new in (("FCsepNBack.tsv", "task-rest_events.tsv"), ("FCsepNBack.json", "task-rest_events.json"), @@ -114,76 +125,89 @@ def SessionEP(scan): source = "{}/{}".format(aux_input, old) if not os.path.isfile(source): logger.error("{}/{}: File {} not found" - .format(sub, ses, source)) + .format(scan.subject, scan.session, source)) def SequenceEP(recording): """ Sequence identification """ - global series - global sid - global Intended - Intended = "" - sid += 1 - recid = series[sid] + + global seq_index + global IntendedFor + IntendedFor = "" + seq_index += 1 + recid = seq_list[seq_index] + + # checking if current sequence corresponds in correct place in list if recid != recording.recId(): logger.warning("{}: Id mismatch folder {}" .format(recording.recIdentity(False), recid)) + + # The inverted fMRI are taken just before normal fMRI + # looking into the following sequence will identify + # the current one if recid == "cmrr_mbep2d_bold_mb2_invertpe": - mod = series[sid + 1] + mod = seq_list[seq_index + 1] if mod.endswith("cmrr_mbep2d_bold_mb2_task_fat"): - Intended = "nBack" + IntendedFor = "nBack" elif mod.endswith("cmrr_mbep2d_bold_mb2_task_nfat"): - Intended = "nBack" + IntendedFor = "nBack" elif mod.endswith("cmrr_mbep2d_bold_mb2_rest"): - Intended = "rest" + IntendedFor = "rest" else: - Intended = "invalid" + IntendedFor = "invalid" logger.warning("{}: Unknown session {}" .format(recording.recIdentity(), mod)) + # fmap images are taken for HCL, LCL and MPM (STROOP) + # sessions elif recid == "gre_field_mapping": if recording.sesId() in ("ses-HCL", "ses-LCL"): - Intended = "HCL/LCL" + IntendedFor = "HCL/LCL" elif recording.sesId() == "ses-STROOP": - Intended = "STROOP" + IntendedFor = "STROOP" else: logger.warning("{}: Unknown session {}" .format(recording.recIdentity(), recording.sesId())) - Intended = "invalid" + IntendedFor = "invalid" + # fmaps sesnsBody and sesnArray are taken just before + # structural PD , T1 and MT. Looking into next sequences + # will allow the identification elif recid == "al_mtflash3d_sensArray": - det = series[sid + 2] + det = seq_list[seq_index + 2] if det.endswith("al_mtflash3d_PDw"): - Intended = "PDw" + IntendedFor = "PDw" elif det.endswith("al_mtflash3d_T1w"): - Intended = "T1w" - recording.setAttribute("Intended", "T1w") + IntendedFor = "T1w" elif det.endswith("al_mtflash3d_MTw"): - Intended = "MTw" + IntendedFor = "MTw" else: logger.warning("{}: Unable determine modality" .format(recording.recIdentity())) - Intended = "invalid" + IntendedFor = "invalid" elif recid == "al_mtflash3d_sensBody": - det = series[sid + 1] + det = seq_list[seq_index + 1] if det.endswith("al_mtflash3d_PDw"): - Intended = "PDw" + IntendedFor = "PDw" elif det.endswith("al_mtflash3d_T1w"): - Intended = "T1w" + IntendedFor = "T1w" elif det.endswith("al_mtflash3d_MTw"): - Intended = "MTw" + IntendedFor = "MTw" else: logger.warning("{}: Unable determine modality" .format(recording.recIdentity())) - Intended = "invalid" + IntendedFor = "invalid" def RecordingEP(recording): - if Intended != "": - recording.setAttribute("SeriesDescription", Intended) + """ + Setting "SeriesDescription" tag for given recording. + """ + if IntendedFor != "": + recording.setAttribute("SeriesDescription", IntendedFor) def SequenceEndEP(outfolder, recording): @@ -192,6 +216,7 @@ def SequenceEndEP(outfolder, recording): """ modality = recording.Modality() + # only for fMRI and diffusion images if modality not in ("func", "dwi"): return @@ -201,8 +226,11 @@ def SequenceEndEP(outfolder, recording): .format(recording.recIdentity(index=False), modality)) first_file = os.path.join(outfolder, recording.files[0]) + # "convertion" is just copy of first file in sequence + # in real application a real external tool should be used shutil.copy2(first_file, f4D + ".nii") first_file = os.path.splitext(first_file)[0] + ".json" + # copying the first file json to allow the identification shutil.copy2(first_file, f4D + ".json") # copying fake bval and bvec values @@ -219,6 +247,8 @@ def SequenceEndEP(outfolder, recording): "NODDI.bvec"), os.path.join(outfolder, "4D.bvec")) + + # Removing now obsolete files for f_nii in recording.files: f_nii = os.path.join(outfolder, f_nii) f_json = os.path.splitext(f_nii)[0] + ".json" diff --git a/example1/resources/plugins/rename_plugin.py b/example1/resources/plugins/rename_plugin.py index 726492895ee2a9dbb0d20b6f7e88e7163bc93a1a..e0be9bffa33c4230e6c8bd9ab383bd1615bd3cec 100644 --- a/example1/resources/plugins/rename_plugin.py +++ b/example1/resources/plugins/rename_plugin.py @@ -25,6 +25,7 @@ preparedfolder = None # switch if is a dry-run (test run) dry_run = False + ########################### # global plugin variables # ###########################