From 93afd942340e94f28e7c24238697877a2b70337b Mon Sep 17 00:00:00 2001 From: MarsMellow Date: Sat, 8 Jul 2023 10:32:35 +0200 Subject: [PATCH] First working version to implement Github PR #19 --- bidscoin/__init__.py | 56 +++++++++++++- bidscoin/bcoin.py | 101 +++---------------------- bidscoin/bids.py | 42 +++++----- bidscoin/bidsapps/deface.py | 42 ++-------- bidscoin/bidsapps/echocombine.py | 39 ++-------- bidscoin/bidsapps/medeface.py | 46 ++--------- bidscoin/bidsapps/skullstrip.py | 40 ++-------- bidscoin/bidsapps/slicereport.py | 72 +++--------------- bidscoin/bidscoiner.py | 63 +++++---------- bidscoin/bidseditor.py | 48 ++++-------- bidscoin/bidsmapper.py | 47 +++--------- bidscoin/cli/__init__.py | 0 bidscoin/cli/_bcoin.py | 49 ++++++++++++ bidscoin/cli/_bidscoiner.py | 35 +++++++++ bidscoin/cli/_bidseditor.py | 36 +++++++++ bidscoin/cli/_bidsmapper.py | 46 +++++++++++ bidscoin/cli/_bidsparticipants.py | 30 ++++++++ bidscoin/cli/_deface.py | 39 ++++++++++ bidscoin/cli/_dicomsort.py | 33 ++++++++ bidscoin/cli/_echocombine.py | 34 +++++++++ bidscoin/cli/_medeface.py | 43 +++++++++++ bidscoin/cli/_physio2tsv.py | 25 ++++++ bidscoin/cli/_plotphysio.py | 25 ++++++ bidscoin/cli/_rawmapper.py | 39 ++++++++++ bidscoin/cli/_skullstrip.py | 36 +++++++++ bidscoin/cli/_slicereport.py | 64 ++++++++++++++++ bidscoin/plugins/dcm2niix2bids.py | 10 +-- bidscoin/plugins/nibabel2bids.py | 7 +- bidscoin/plugins/pet2bids.py | 6 +- bidscoin/plugins/phys2bidscoin.py | 7 +- bidscoin/plugins/spec2nii2bids.py | 7 +- bidscoin/utilities/bidsparticipants.py | 37 ++------- bidscoin/utilities/dicomsort.py | 39 ++-------- bidscoin/utilities/physio2tsv.py | 35 +++------ bidscoin/utilities/plotphysio.py | 35 +++------ bidscoin/utilities/rawmapper.py | 45 ++--------- docs/CHANGELOG.md | 2 +- docs/workflow.rst | 2 +- pyproject.toml | 20 ++++- setup.py | 5 +- tests/conftest.py | 9 +-- tests/test_bcoin.py | 23 +++--- tests/test_bids.py | 10 +-- tests/test_bidscoiner.py | 10 +-- tests/test_bidsmapper.py | 10 +-- tests/test_load_bidsmap.py | 21 +++++ tests/test_pet2bids.py | 31 -------- tests/test_plugins.py | 8 +- tests/test_utilities.py | 11 +-- tox.ini | 4 +- 50 files changed, 817 insertions(+), 707 deletions(-) create mode 100644 bidscoin/cli/__init__.py create mode 100755 bidscoin/cli/_bcoin.py create mode 100755 bidscoin/cli/_bidscoiner.py create mode 100755 bidscoin/cli/_bidseditor.py create mode 100755 bidscoin/cli/_bidsmapper.py create mode 100755 bidscoin/cli/_bidsparticipants.py create mode 100755 bidscoin/cli/_deface.py create mode 100755 bidscoin/cli/_dicomsort.py create mode 100755 bidscoin/cli/_echocombine.py create mode 100755 bidscoin/cli/_medeface.py create mode 100755 bidscoin/cli/_physio2tsv.py create mode 100755 bidscoin/cli/_plotphysio.py create mode 100755 bidscoin/cli/_rawmapper.py create mode 100755 bidscoin/cli/_skullstrip.py create mode 100755 bidscoin/cli/_slicereport.py create mode 100644 tests/test_load_bidsmap.py delete mode 100644 tests/test_pet2bids.py diff --git a/bidscoin/__init__.py b/bidscoin/__init__.py index e54a1f02..6fa33e11 100644 --- a/bidscoin/__init__.py +++ b/bidscoin/__init__.py @@ -14,15 +14,67 @@ SPDX-License-Identifier: GPL-3.0-or-later """ +import urllib.request +import json from pathlib import Path -from importlib.metadata import version +from importlib.metadata import version as libversion +from typing import Tuple, Union try: import tomllib except ModuleNotFoundError: import tomli as tomllib +try: + from .due import due, Doi +except ImportError: + from due import due, Doi + +# Add the BIDScoin citation +due.cite(Doi('10.3389/fninf.2021.770608'), description='A toolkit to convert source data to the Brain Imaging Data Structure (BIDS)', path='bidscoin') +# Get the BIDScoin version try: - __version__ = version('bidscoin') + __version__ = libversion('bidscoin') except Exception: with open(Path(__file__).parents[1]/'pyproject.toml', 'rb') as fid: __version__ = tomllib.load(fid)['project']['version'] + +# Define the default paths +tutorialurl = 'https://surfdrive.surf.nl/files/index.php/s/HTxdUbykBZm2cYM/download' +bidscoinfolder = Path(__file__).parent +schemafolder = bidscoinfolder/'schema' +heuristicsfolder = bidscoinfolder/'heuristics' +pluginfolder = bidscoinfolder/'plugins' +bidsmap_template = heuristicsfolder/'bidsmap_dccn.yaml' # Default template bidsmap TODO: make it a user setting (in $HOME)? + + +def version(check: bool=False) -> Union[str, Tuple]: + """ + Reads the BIDSCOIN version from the VERSION.TXT file and from pypi + + :param check: Check if the current version is up-to-date + :return: The version number or (version number, checking message) if check=True + """ + + # Check pypi for the latest version number + if check: + try: + stream = urllib.request.urlopen('https://pypi.org/pypi/bidscoin/json').read() + pypiversion = json.loads(stream)['info']['version'] + except Exception as pypierror: + return __version__, "(Could not check for new BIDScoin versions)" + if __version__ != pypiversion: + return __version__, f"NB: Your BIDScoin version is NOT up-to-date: {__version__} -> {pypiversion}" + else: + return __version__, "Your BIDScoin version is up-to-date :-)" + + return __version__ + + +def bidsversion() -> str: + """ + Reads the BIDS version from the BIDS_VERSION.TXT file + + :return: The BIDS version number + """ + + return (schemafolder/'BIDS_VERSION').read_text().strip() diff --git a/bidscoin/bcoin.py b/bidscoin/bcoin.py index 6f9d2f8b..44a82312 100755 --- a/bidscoin/bcoin.py +++ b/bidscoin/bcoin.py @@ -1,57 +1,32 @@ #!/usr/bin/env python3 """ -BIDScoin is a toolkit to convert and organize raw data-sets according to the Brain Imaging Data Structure (BIDS) +A BIDScoin library and application with utilities to perform generic management tasks (See also cli/_bcoin.py) -The basic workflow is to run these two tools: - - $ bidsmapper sourcefolder bidsfolder # This produces a study bidsmap and launches a GUI - $ bidscoiner sourcefolder bidsfolder # This converts your data to BIDS according to the study bidsmap - -Set the environment variable BIDSCOIN_DEBUG=TRUE in your console to run BIDScoin in its more verbose DEBUG logging mode - -For more documentation see: https://bidscoin.readthedocs.io +@author: Marcel Zwiers """ -import argparse import coloredlogs import inspect -import json import logging import os import shutil import subprocess import sys -import textwrap import urllib.request from functools import lru_cache -from importlib.metadata import entry_points, version as libversion +from importlib.metadata import entry_points from importlib.util import spec_from_file_location, module_from_spec from pathlib import Path from typing import Tuple, Union, List from ruamel.yaml import YAML from tqdm import tqdm -try: - import tomllib -except ModuleNotFoundError: - import tomli as tomllib -try: - from .due import due, Doi -except ImportError: - from due import due, Doi +from importlib.util import find_spec +if find_spec('bidscoin') is None: + sys.path.append(str(Path(__file__).parents[1])) +from bidscoin import heuristicsfolder, pluginfolder, bidsmap_template, tutorialurl yaml = YAML() -# Add the BIDScoin citation -due.cite(Doi('10.3389/fninf.2021.770608'), description='A toolkit to convert source data to the Brain Imaging Data Structure (BIDS)', path='bidscoin') - -# Define the default paths -tutorialurl = 'https://surfdrive.surf.nl/files/index.php/s/HTxdUbykBZm2cYM/download' -bidscoinfolder = Path(__file__).parent -schemafolder = bidscoinfolder/'schema' -heuristicsfolder = bidscoinfolder/'heuristics' -pluginfolder = bidscoinfolder/'plugins' -bidsmap_template = heuristicsfolder/'bidsmap_dccn.yaml' # Default template bidsmap TODO: make it a user setting (in $HOME)? - # Get the BIDSCOIN_DEBUG environment variable to set the log-messages and logging level, etc debug = os.environ.get('BIDSCOIN_DEBUG') debug = True if debug and debug.upper() not in ('0', 'FALSE', 'N', 'NO', 'NONE') else False @@ -182,46 +157,6 @@ def reporterrors() -> str: return errors -def version(check: bool=False) -> Union[str, Tuple]: - """ - Reads the BIDSCOIN version from the VERSION.TXT file and from pypi - - :param check: Check if the current version is up-to-date - :return: The version number or (version number, checking message) if check=True - """ - - try: - localversion = libversion('bidscoin') - except Exception: - with open(Path(__file__).parents[1]/'pyproject.toml', 'rb') as fid: - localversion = tomllib.load(fid)['project']['version'] - - # Check pypi for the latest version number - if check: - try: - stream = urllib.request.urlopen('https://pypi.org/pypi/bidscoin/json').read() - pypiversion = json.loads(stream)['info']['version'] - except Exception as pypierror: - LOGGER.info(f"Checking BIDScoin version on https://pypi.org/pypi/bidscoin failed:\n{pypierror}") - return localversion, "(Could not check for new BIDScoin versions)" - if localversion != pypiversion: - return localversion, f"NB: Your BIDScoin version is NOT up-to-date: {localversion} -> {pypiversion}" - else: - return localversion, "Your BIDScoin version is up-to-date :-)" - - return localversion - - -def bidsversion() -> str: - """ - Reads the BIDS version from the BIDSVERSION.TXT file - - :return: The BIDS version number - """ - - return (schemafolder/'BIDS_VERSION').read_text().strip() - - def run_command(command: str) -> int: """ Runs a command in a shell using subprocess.run(command, ..) @@ -654,28 +589,12 @@ def pulltutorialdata(tutorialfolder: str) -> None: def main(): """Console script usage""" + from bidscoin.cli._bcoin import get_parser + setup_logging() - localversion, versionmessage = version(check=True) # Parse the input arguments and run bidscoiner(args) - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=textwrap.dedent(__doc__), - epilog='examples:\n' - ' bidscoin -l\n' - ' bidscoin -d data/bidscoin_tutorial\n' - ' bidscoin -t\n' - ' bidscoin -t my_template_bidsmap\n' - ' bidscoin -b my_study_bidsmap\n' - ' bidscoin -i data/my_template_bidsmap.yaml downloads/my_plugin.py\n ') - parser.add_argument('-l', '--list', help='List all executables (i.e. the apps, bidsapps and utilities)', action='store_true') - parser.add_argument('-p', '--plugins', help='List all installed plugins and template bidsmaps', action='store_true') - parser.add_argument('-i', '--install', help='A list of template bidsmaps and/or bidscoin plugins to install', nargs='+') - parser.add_argument('-u', '--uninstall', help='A list of template bidsmaps and/or bidscoin plugins to uninstall', nargs='+') - parser.add_argument('-d', '--download', help='Download folder. If given, tutorial MRI data will be downloaded here') - parser.add_argument('-t', '--test', help='Test the bidscoin installation and template bidsmap', nargs='?', const=bidsmap_template) - parser.add_argument('-b', '--bidsmaptest', help='Test the run-items and their bidsnames of all normal runs in the study bidsmap. Provide the bids-folder or the bidsmap filepath') - parser.add_argument('-v', '--version', help='Show the installed version and check for updates', action='version', version=f"BIDS-version:\t\t{bidsversion()}\nBIDScoin-version:\t{localversion}, {versionmessage}") - args = parser.parse_args(None if sys.argv[1:] else ['--help']) + args = get_parser().parse_args(None if sys.argv[1:] else ['--help']) list_executables(show=args.list) list_plugins(show=args.plugins) diff --git a/bidscoin/bids.py b/bidscoin/bids.py index 779546d7..0f1a6d3c 100644 --- a/bidscoin/bids.py +++ b/bidscoin/bids.py @@ -1,5 +1,5 @@ """ -Module with helper functions +BIDScoin module with bids/bidsmap related helper functions Some functions are derived from dac2bids.py from Daniel Gomez 29.08.2016 https://github.com/dangom/dac2bids/blob/master/dac2bids.py @@ -21,33 +21,31 @@ from typing import Union, List, Tuple from nibabel.parrec import parse_PAR_header from pydicom import dcmread, fileset, datadict -try: - from bidscoin import bcoin - from bidscoin.utilities import dicomsort -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parent/'utilities')) # This should work if bidscoin was not pip-installed - import bcoin - import dicomsort + sys.path.append(str(Path(__file__).parents[1])) +from bidscoin import bcoin, schemafolder, __version__ +from bidscoin.utilities import dicomsort from ruamel.yaml import YAML yaml = YAML() LOGGER = logging.getLogger(__name__) # Read the BIDS schema data -with (bcoin.schemafolder/'objects'/'datatypes.yaml').open('r') as _stream: +with (schemafolder/'objects'/'datatypes.yaml').open('r') as _stream: bidsdatatypesdef = yaml.load(_stream) # The valid BIDS datatypes, along with their full names and descriptions datatyperules = {} -for _datatypefile in (bcoin.schemafolder/'rules'/'files'/'raw').glob('*.yaml'): +for _datatypefile in (schemafolder/'rules'/'files'/'raw').glob('*.yaml'): with _datatypefile.open('r') as _stream: datatyperules[_datatypefile.stem] = yaml.load(_stream) # The entities that can/should be present for each BIDS datatype -with (bcoin.schemafolder/'objects'/'suffixes.yaml').open('r') as _stream: +with (schemafolder/'objects'/'suffixes.yaml').open('r') as _stream: suffixes = yaml.load(_stream) # The descriptions of the valid BIDS file suffixes -with (bcoin.schemafolder/'objects'/'entities.yaml').open('r') as _stream: +with (schemafolder/'objects'/'entities.yaml').open('r') as _stream: entities = yaml.load(_stream) # The descriptions of the entities present in BIDS filenames -with (bcoin.schemafolder/'rules'/'entities.yaml').open('r') as _stream: +with (schemafolder/'rules'/'entities.yaml').open('r') as _stream: entitiesorder = yaml.load(_stream) # The order in which the entities should appear within filenames -with (bcoin.schemafolder/'objects'/'metadata.yaml').open('r') as _stream: +with (schemafolder/'objects'/'metadata.yaml').open('r') as _stream: metafields = yaml.load(_stream) # The descriptions of the valid BIDS metadata fields @@ -884,10 +882,10 @@ def load_bidsmap(yamlfile: Path, folder: Path=Path(), plugins:Union[tuple,list]= bidsmapversion = bidsmap['Options']['version'] else: bidsmapversion = 'Unknown' - if bidsmapversion.rsplit('.', 1)[0] != bcoin.version().rsplit('.', 1)[0] and any(check): - LOGGER.warning(f'BIDScoiner version conflict: {yamlfile} was created with version {bidsmapversion}, but this is version {bcoin.version()}') - elif bidsmapversion != bcoin.version() and any(check): - LOGGER.info(f'BIDScoiner version difference: {yamlfile} was created with version {bidsmapversion}, but this is version {bcoin.version()}. This is normally ok but check the https://bidscoin.readthedocs.io/en/latest/CHANGELOG.html') + if bidsmapversion.rsplit('.', 1)[0] != __version__.rsplit('.', 1)[0] and any(check): + LOGGER.warning(f'BIDScoiner version conflict: {yamlfile} was created with version {bidsmapversion}, but this is version {__version__}') + elif bidsmapversion != __version__ and any(check): + LOGGER.info(f'BIDScoiner version difference: {yamlfile} was created with version {bidsmapversion}, but this is version {__version__}. This is normally ok but check the https://bidscoin.readthedocs.io/en/latest/CHANGELOG.html') # Make sure we get a proper plugin options and dataformat sections (use plugin default bidsmappings when a template bidsmap is loaded) if not bidsmap['Options'].get('plugins'): @@ -1213,14 +1211,14 @@ def check_run(datatype: str, run: dict, check: Tuple[bool, bool, bool]=(False, F return run_keysok, run_suffixok, run_valsok -def check_ignore(entry: str, bidsignore: Union[str,list], type: str='dir') -> bool: +def check_ignore(entry: str, bidsignore: Union[str,list], datatype: str= 'dir') -> bool: """ A rudimentary check whether `entry` should be BIDS-ignored. This function should eventually be replaced by bids_validator functionality See also https://github.com/bids-standard/bids-specification/issues/131 :param entry: The entry that is checked against the bidsignore (e.g. a directory/datatype such as `anat` or a file such as `sub-001_ct.nii.gz`) :param bidsignore: The list or semicolon separated bidsignore pattern (e.g. from the bidscoin Options such as `mrs/;extra_data/;sub-*_ct.*`) - :param type: The entry type, i.e. 'dir' or 'file', that can be used to limit the check + :param datatype: The entry datatype, i.e. 'dir' or 'file', that can be used to limit the check :return: True if the entry should be ignored, else False """ @@ -1238,8 +1236,8 @@ def check_ignore(entry: str, bidsignore: Union[str,list], type: str='dir') -> bo ignore = False for item in bidsignore: - if type == 'dir' and not item.endswith('/'): continue - if type == 'file' and item.endswith('/'): continue + if datatype =='dir' and not item.endswith('/'): continue + if datatype =='file' and item.endswith('/'): continue if item.endswith('/'): item = item[0:-1] if fnmatch.fnmatch(entry, item): diff --git a/bidscoin/bidsapps/deface.py b/bidscoin/bidsapps/deface.py index dc851618..ccef078d 100755 --- a/bidscoin/bidsapps/deface.py +++ b/bidscoin/bidsapps/deface.py @@ -1,19 +1,8 @@ #!/usr/bin/env python3 -""" -A wrapper around the 'pydeface' defacing tool (https://github.com/poldracklab/pydeface). - -Except for BIDS inheritances and IntendedFor usage, this wrapper is BIDS-aware (a 'bidsapp') -and writes BIDS compliant output - -Linux users can distribute the computations to their HPC compute cluster if the DRMAA -libraries are installed and the DRMAA_LIBRARY_PATH environment variable set - -For multi-echo data see `medeface` -""" +"""A bidsapp that wraps around the 'pydeface' defacing tool (See also cli/_deface.py)""" import os import shutil -import argparse import json import logging import pandas as pd @@ -21,12 +10,11 @@ from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bcoin, bids def deface(bidsdir: str, pattern: str, subjects: list, force: bool, output: str, cluster: bool, nativespec: str, kwargs: dict): @@ -162,25 +150,9 @@ def deface(bidsdir: str, pattern: str, subjects: list, force: bool, output: str, def main(): """Console script usage""" - class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass - - parser = argparse.ArgumentParser(formatter_class=CustomFormatter, - description=__doc__, - epilog='examples:\n' - ' deface myproject/bids anat/*_T1w*\n' - ' deface myproject/bids anat/*_T1w* -p 001 003 -o derivatives\n' - ' deface myproject/bids anat/*_T1w* -c -n "-l walltime=00:60:00,mem=4gb"\n' - ' deface myproject/bids anat/*_T1w* -a \'{"cost": "corratio", "verbose": ""}\'\n ') - parser.add_argument('bidsfolder', help='The bids-directory with the subject data') - parser.add_argument('pattern', help="Globlike search pattern (relative to the subject/session folder) to select the images that need to be defaced, e.g. 'anat/*_T1w*'") - parser.add_argument('-p','--participant_label', help='Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed', nargs='+') - parser.add_argument('-o','--output', help=f"A string that determines where the defaced images are saved. It can be the name of a BIDS datatype folder, such as 'anat', or of the derivatives folder, i.e. 'derivatives'. If output is left empty then the original images are replaced by the defaced images") - parser.add_argument('-c','--cluster', help='Use the DRMAA library to submit the deface jobs to a high-performance compute (HPC) cluster', action='store_true') - parser.add_argument('-n','--nativespec', help='DRMAA native specifications for submitting deface jobs to the HPC cluster', default='-l walltime=00:30:00,mem=2gb') - parser.add_argument('-a','--args', help='Additional arguments (in dict/json-style) that are passed to pydeface. See examples for usage', type=json.loads, default={}) - parser.add_argument('-f','--force', help='Deface all images, regardless if they have already been defaced (i.e. if {"Defaced": True} in the json sidecar file)', action='store_true') - args = parser.parse_args() + from bidscoin.cli._deface import get_parser + args = get_parser().parse_args() deface(bidsdir = args.bidsfolder, pattern = args.pattern, subjects = args.participant_label, diff --git a/bidscoin/bidsapps/echocombine.py b/bidscoin/bidsapps/echocombine.py index 0e055e07..a21a6385 100755 --- a/bidscoin/bidsapps/echocombine.py +++ b/bidscoin/bidsapps/echocombine.py @@ -1,13 +1,6 @@ #!/usr/bin/env python3 -""" -A wrapper around the 'mecombine' multi-echo combination tool -(https://github.com/Donders-Institute/multiecho). +"""A bidsapp that wraps around the multi-echo combination tool (See also cli/_echocombine.py)""" -Except for BIDS inheritances, this wrapper is BIDS-aware (a 'bidsapp') and writes BIDS -compliant output -""" - -import argparse import json import logging import pandas as pd @@ -15,12 +8,11 @@ from tqdm.contrib.logging import logging_redirect_tqdm from multiecho import combination as me from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bcoin, bids unknowndatatype = 'extra_data' @@ -206,25 +198,10 @@ def echocombine(bidsdir: str, pattern: str, subjects: list, output: str, algorit def main(): """Console script usage""" - class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass - - parser = argparse.ArgumentParser(formatter_class=CustomFormatter, - description=__doc__, - epilog='examples:\n' - ' echocombine myproject/bids func/*task-stroop*echo-1*\n' - ' echocombine myproject/bids *task-stroop*echo-1* -p 001 003\n' - ' echocombine myproject/bids func/*task-*echo-1* -o func\n' - ' echocombine myproject/bids func/*task-*echo-1* -o derivatives -w 13 26 39 52\n' - ' echocombine myproject/bids func/*task-*echo-1* -a PAID\n ') - parser.add_argument('bidsfolder', help='The bids-directory with the (multi-echo) subject data') - parser.add_argument('pattern', help="Globlike recursive search pattern (relative to the subject/session folder) to select the first echo of the images that need to be combined, e.g. '*task-*echo-1*'") - parser.add_argument('-p','--participant_label', help='Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed', nargs='+') - parser.add_argument('-o','--output', help=f"A string that determines where the output is saved. It can be the name of a BIDS datatype folder, such as 'func', or of the derivatives folder, i.e. 'derivatives'. If output = [the name of the input datatype folder] then the original echo images are replaced by one combined image. If output is left empty then the combined image is saved in the input datatype folder and the original echo images are moved to the {unknowndatatype} folder", default='') - parser.add_argument('-a','--algorithm', help='Combination algorithm', choices=['PAID', 'TE', 'average'], default='TE') - parser.add_argument('-w','--weights', help='Weights for each echo', nargs='*', default=None, type=list) - parser.add_argument('-f','--force', help='Process all images, regardless whether target images already exist. Otherwise the echo-combination will be skipped', action='store_true') - args = parser.parse_args() + from bidscoin.cli._echocombine import get_parser + # Parse the input arguments and run bidscoiner(args) + args = get_parser().parse_args() echocombine(bidsdir = args.bidsfolder, pattern = args.pattern, subjects = args.participant_label, diff --git a/bidscoin/bidsapps/medeface.py b/bidscoin/bidsapps/medeface.py index ef20de24..db860802 100755 --- a/bidscoin/bidsapps/medeface.py +++ b/bidscoin/bidsapps/medeface.py @@ -1,21 +1,8 @@ #!/usr/bin/env python3 -""" -A wrapper around the 'pydeface' defacing tool (https://github.com/poldracklab/pydeface) that -computes a defacing mask on a (temporary) echo-combined image and then applies it to each -individual echo-image. - -Except for BIDS inheritances and IntendedFor usage, this wrapper is BIDS-aware (a 'bidsapp') -and writes BIDS compliant output - -Linux users can distribute the computations to their HPC compute cluster if the DRMAA -libraries are installed and the DRMAA_LIBRARY_PATH environment variable set - -For single-echo data see `deface` -""" +"""A bidsapp that combines echos and wraps around the 'pydeface' defacing tool (See also cli/_medeface.py)""" import os import shutil -import argparse import json import logging import pandas as pd @@ -26,12 +13,11 @@ from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bcoin, bids def medeface(bidsdir: str, pattern: str, maskpattern: str, subjects: list, force: bool, output: str, cluster: bool, nativespec: str, kwargs: dict): @@ -212,27 +198,9 @@ def medeface(bidsdir: str, pattern: str, maskpattern: str, subjects: list, force def main(): """Console script usage""" - class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass - - parser = argparse.ArgumentParser(formatter_class=CustomFormatter, - description=__doc__, - epilog='examples:\n' - ' medeface myproject/bids anat/*_T1w*\n' - ' medeface myproject/bids anat/*_T1w* -p 001 003 -o derivatives\n' - ' medeface myproject/bids anat/*_T1w* -c -n "-l walltime=00:60:00,mem=4gb"\n' - ' medeface myproject/bids anat/*acq-GRE* -m anat/*acq-GRE*magnitude*"\n' - ' medeface myproject/bids anat/*_FLAIR* -a \'{"cost": "corratio", "verbose": ""}\'\n ') - parser.add_argument('bidsfolder', help='The bids-directory with the (multi-echo) subject data') - parser.add_argument('pattern', help="Globlike search pattern (relative to the subject/session folder) to select the images that need to be defaced, e.g. 'anat/*_T2starw*'") - parser.add_argument('-m','--maskpattern', help="Globlike search pattern (relative to the subject/session folder) to select the images from which the defacemask is computed, e.g. 'anat/*_part-mag_*_T2starw*'. If not given then 'pattern' is used") - parser.add_argument('-p','--participant_label', help='Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed', nargs='+') - parser.add_argument('-o','--output', help=f"A string that determines where the defaced images are saved. It can be the name of a BIDS datatype folder, such as 'anat', or of the derivatives folder, i.e. 'derivatives'. If output is left empty then the original images are replaced by the defaced images") - parser.add_argument('-c','--cluster', help='Submit the deface jobs to a high-performance compute (HPC) cluster', action='store_true') - parser.add_argument('-n','--nativespec', help='DRMAA native specifications for submitting deface jobs to the HPC cluster', default='-l walltime=00:30:00,mem=2gb') - parser.add_argument('-a','--args', help='Additional arguments (in dict/json-style) that are passed to pydeface. See examples for usage', type=json.loads, default={}) - parser.add_argument('-f','--force', help='Process all images, regardless if images have already been defaced (i.e. if {"Defaced": True} in the json sidecar file)', action='store_true') - args = parser.parse_args() + from bidscoin.cli._medeface import get_parser + args = get_parser().parse_args() medeface(bidsdir = args.bidsfolder, pattern = args.pattern, maskpattern = args.maskpattern, diff --git a/bidscoin/bidsapps/skullstrip.py b/bidscoin/bidsapps/skullstrip.py index ad9eb9e1..5f84c05c 100755 --- a/bidscoin/bidsapps/skullstrip.py +++ b/bidscoin/bidsapps/skullstrip.py @@ -1,16 +1,7 @@ #!/usr/bin/env python3 -""" -A wrapper around FreeSurfer's 'synthstrip' skull stripping tool -(https://surfer.nmr.mgh.harvard.edu/docs/synthstrip). Except for BIDS inheritances, -this wrapper is BIDS-aware (a 'bidsapp') and writes BIDS compliant output - -The corresponding brain mask is saved in the bids/derivatives/synthstrip folder - -Assumes the installation of FreeSurfer v7.3.2 or higher -""" +"""A bidsapp that combines echos and wraps around FreeSurfer's 'synthstrip' skull stripping tool (See also cli/_skullstrip.py)""" import shutil -import argparse import json import logging import pandas as pd @@ -18,12 +9,11 @@ from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bcoin, bids def skullstrip(bidsdir: str, pattern: str, subjects: list, masked: str, output: list, force: bool, args: str, cluster: bool): @@ -216,25 +206,9 @@ def skullstrip(bidsdir: str, pattern: str, subjects: list, masked: str, output: def main(): """Console script usage""" - class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass - - parser = argparse.ArgumentParser(formatter_class=CustomFormatter, - description=__doc__, - epilog='examples:\n' - ' skullstrip myproject/bids anat/*_T1w*\n' - ' skullstrip myproject/bids anat/*_T1w* -p 001 003 -a \' --no-csf\'\n' - ' skullstrip myproject/bids fmap/*_magnitude1* -m fmap/*_phasediff* -o extra_data fmap\n' - ' skullstrip myproject/bids fmap/*_acq-mylabel*_magnitude1* -m fmap/*_acq-mylabel_* -o fmap\n ') - parser.add_argument('bidsfolder', help="The bids-directory with the subject data", type=str) - parser.add_argument('pattern', help="Globlike search pattern (relative to the subject/session folder) to select the (3D) images that need to be skullstripped, e.g. 'anat/*_T1w*'", type=str) - parser.add_argument('-p','--participant_label', help="Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed", type=str, nargs='+') - parser.add_argument('-m','--masked', help="Globlike search pattern (relative to the subject/session folder) to select additional (3D/4D) images from the same space that need to be masked with the same mask, e.g. 'fmap/*_phasediff'. NB: This option can only be used if pattern yields a single file per session", type=str) - parser.add_argument('-o','--output', help="One or two output strings that determine where the skullstripped + additional masked images are saved. Each output string can be the name of a BIDS datatype folder, such as 'anat', or of the derivatives folder, i.e. 'derivatives' (default). If the output string is the same as the datatype then the original images are replaced by the skullstripped images", nargs='+') - parser.add_argument('-f','--force', help="Process images, regardless whether images have already been skullstripped (i.e. if {'SkullStripped': True} in the json sidecar file)", action='store_true') - parser.add_argument('-a','--args', help="Additional arguments that are passed to synthstrip (NB: Use quotes and a leading space to prevent unintended argument parsing)", type=str, default='') - parser.add_argument('-c','--cluster', help='Use `qsub` to submit the skullstrip jobs to a high-performance compute (HPC) cluster. Can only be used if `--masked` is left empty', action='store_true') - args = parser.parse_args() + from bidscoin.cli._skullstrip import get_parser + args = get_parser().parse_args() skullstrip(bidsdir = args.bidsfolder, pattern = args.pattern, subjects = args.participant_label, diff --git a/bidscoin/bidsapps/slicereport.py b/bidscoin/bidsapps/slicereport.py index ffedca48..638aa2b0 100755 --- a/bidscoin/bidsapps/slicereport.py +++ b/bidscoin/bidsapps/slicereport.py @@ -1,16 +1,6 @@ #!/usr/bin/env python3 -""" -A wrapper around the 'slicer' imaging tool (https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/Miscvis) -to generate a web page with a row of image slices for each subject in the BIDS repository, as -well as individual sub-pages displaying more detailed information. The input images are -selectable using wildcards, and the output images are configurable via various user options, -allowing you to quickly create a custom 'slicer' report to do visual quality control on any -datatype in your repository. - -Requires an existing installation of FSL/slicer -""" +"""A wrapper around the 'slicer' imaging tool (See also cli/_slicereport.py)""" -import argparse import logging import subprocess import sys @@ -19,11 +9,10 @@ import tempfile from copy import copy from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids +from importlib.util import find_spec +if find_spec('bidscoin') is None: + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bcoin, bids, bidsversion, __version__ html_head = """ @@ -259,9 +248,9 @@ def slicereport(bidsdir: str, pattern: str, outlinepattern: str, outlineimage: s dataset = reportdir/'dataset_description.json' if not dataset.is_file(): description = {"Name": "Slicereport - A visual inspection report", - "BIDSVersion": bcoin.bidsversion(), + "BIDSVersion": bidsversion(), "DatasetType": "derivative", - "GeneratedBy": [{"Name":"BIDScoin", "Version":bcoin.version(), "CodeURL":"https://github.com/Donders-Institute/bidscoin"}]} + "GeneratedBy": [{"Name":"BIDScoin", "Version":__version__, "CodeURL":"https://github.com/Donders-Institute/bidscoin"}]} with dataset.open('w') as fid: json.dump(description, fid, indent=4) @@ -287,52 +276,9 @@ def slicereport(bidsdir: str, pattern: str, outlinepattern: str, outlineimage: s def main(): """Console script usage""" - epilogue = """ -OPTIONS: - L : Label slices with slice number. - l [LUT] : Use a different colour map from that specified in the header. - i [MIN] [MAX] : Specify intensity min and max for display range. - e [THR] : Use the specified threshold for edges (if > 0 use this proportion of max-min, - if < 0, use the absolute value) - t : Produce semi-transparent (dithered) edges. - n : Use nearest-neighbour interpolation for output. - u : Do not put left-right labels in output. - s : Size scaling factor - c : Add a red dot marker to top right of image - -OUTPUTS: - x/y/z [SLICE] [..] : Output sagittal, coronal or axial slice (if [SLICE] > 0 it is a - fraction of image dimension, if < 0, it is an absolute slice number) - a : Output mid-sagittal, -coronal and -axial slices into one image - A [WIDTH] : Output _all_ axial slices into one image of _max_ width [WIDTH] - S [SAMPLE] [WIDTH] : As `A` but only include every [SAMPLE]'th slice - LF : Start a new line (i.e. works like a row break) - -examples: - slicereport myproject/bids anat/*_T1w* - slicereport myproject/bids anat/*_T2w* -r myproject/QC/slicereport_T2 -x myproject/QC/slicereport_T1 - slicereport myproject/bids fmap/*_phasediff* -o fmap/*_magnitude1* - slicereport myproject/bids/derivatives/fmriprep anat/*run-?_desc-preproc_T1w* -o anat/*run-?_label-GM* - slicereport myproject/bids/derivatives/deface anat/*_T1w* -o myproject/bids:anat/*_T1w* --options L e 0.05 - slicereport myproject/bids anat/*_T1w* --outputs x 0.3 x 0.4 x 0.5 x 0.6 x 0.7 LF z 0.3 z 0.4 z 0.5 z 0.6 z 0.7\n """ - - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=__doc__, epilog=epilogue) - parser.add_argument('bidsfolder', help='The bids-directory with the subject data') - parser.add_argument('pattern', help="Globlike search pattern to select the images in bidsfolder to be reported, e.g. 'anat/*_T2starw*'") - parser.add_argument('-o','--outlinepattern', help="Globlike search pattern to select red outline images that are projected on top of the reported images (i.e. 'outlinepattern' must yield the same number of images as 'pattern'. Prepend `outlinedir:` if your outline images are in `outlinedir` instead of `bidsdir` (see examples below)`") - parser.add_argument('-i','--outlineimage', help='A common red-outline image that is projected on top of all images', default='') - parser.add_argument('-p','--participant_label', help='Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed', nargs='+') - parser.add_argument('-r','--reportfolder', help="The folder where the report is saved (default: bidsfolder/derivatives/slicereport)") - parser.add_argument('-x','--xlinkfolder', help="A (list of) QC report folder(s) with cross-linkable sub-reports, e.g. bidsfolder/derivatives/mriqc", nargs='+') - parser.add_argument('-q','--qcscores', help="Column names for creating an accompanying tsv-file to store QC-rating scores (default: rating_overall)", default=['rating_overall'], nargs='+') - parser.add_argument('-c','--cluster', help='Use `qsub` to submit the slicer jobs to a high-performance compute (HPC) cluster', action='store_true') - parser.add_argument('--options', help='Main options of slicer (see below). (default: "s 1")', default=['s','1'], nargs='+') - parser.add_argument('--outputs', help='Output options of slicer (see below). (default: "x 0.4 x 0.5 x 0.6 y 0.4 y 0.5 y 0.6 z 0.4 z 0.5 z 0.6")', default=['x','0.4','x','0.5','x','0.6','y','0.4','y','0.5','y','0.6','z','0.4','z','0.5','z','0.6'], nargs='+') - parser.add_argument('--suboptions', help='Main options of slicer for creating the sub-reports (same as OPTIONS, see below). (default: OPTIONS)', nargs='+') - parser.add_argument('--suboutputs', help='Output options of slicer for creating the sub-reports (same as OUTPUTS, see below). (default: "S 4 1600")', default=['S','4','1600'], nargs='+') - args = parser.parse_args() + from bidscoin.cli._slicereport import get_parser + args = get_parser().parse_args() slicereport(bidsdir = args.bidsfolder, pattern = args.pattern, outlinepattern = args.outlinepattern, diff --git a/bidscoin/bidscoiner.py b/bidscoin/bidscoiner.py index 7cc7827e..c4e69894 100755 --- a/bidscoin/bidscoiner.py +++ b/bidscoin/bidscoiner.py @@ -1,22 +1,7 @@ #!/usr/bin/env python3 -""" -Converts ("coins") your source datasets to NIfTI/json/tsv BIDS datasets using the mapping -information from the bidsmap.yaml file. Edit this bidsmap to your needs using the bidseditor -tool before running this function or (re-)run the bidsmapper whenever you encounter unexpected -data. You can run bidscoiner after all data has been collected, or run / re-run it whenever -new data has been added to your source folder (presuming the scan protocol hasn't changed). -Also, if you delete a subject/session folder from the bidsfolder, it will simply be re-created -from the sourcefolder the next time you run the bidscoiner. - -The bidscoiner uses plugins, as stored in the bidsmap['Options'], to do the actual work - -Provenance information, warnings and error messages are stored in the -bidsfolder/code/bidscoin/bidscoiner.log file. -""" - -import argparse +"""A BIDScoin application to convert source data to BIDS (See also cli/_bidscoiner.py)""" + import dateutil.parser -import textwrap import re import pandas as pd import json @@ -26,12 +11,11 @@ from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: - import bcoin, bids # This should work if bidscoin was not pip-installed - -localversion, _ = bcoin.version(check=True) +from importlib.util import find_spec +if find_spec('bidscoin') is None: + import sys + sys.path.append(str(Path(__file__).parents[1])) +from bidscoin import bcoin, bids, bidsversion, __version__ def bidscoiner(rawfolder: str, bidsfolder: str, subjects: list=(), force: bool=False, bidsmapfile: str='bidsmap.yaml') -> None: @@ -55,7 +39,7 @@ def bidscoiner(rawfolder: str, bidsfolder: str, subjects: list=(), force: bool=F # Start logging bcoin.setup_logging(bidsfolder/'code'/'bidscoin'/'bidscoiner.log') LOGGER.info('') - LOGGER.info(f"-------------- START BIDScoiner {localversion}: BIDS {bcoin.bidsversion()} ------------") + LOGGER.info(f"-------------- START BIDScoiner {__version__}: BIDS {bidsversion()} ------------") LOGGER.info(f">>> bidscoiner sourcefolder={rawfolder} bidsfolder={bidsfolder} subjects={subjects} force={force} bidsmap={bidsmapfile}") # Create a code/bidscoin subfolder @@ -63,12 +47,12 @@ def bidscoiner(rawfolder: str, bidsfolder: str, subjects: list=(), force: bool=F # Create a dataset description file if it does not exist dataset_file = bidsfolder/'dataset_description.json' - generatedby = [{"Name":"BIDScoin", "Version":localversion, "CodeURL":"https://github.com/Donders-Institute/bidscoin"}] + generatedby = [{"Name":"BIDScoin", "Version":__version__, "CodeURL":"https://github.com/Donders-Institute/bidscoin"}] if not dataset_file.is_file(): LOGGER.info(f"Creating dataset description file: {dataset_file}") dataset_description = {"Name": "REQUIRED. Name of the dataset", "GeneratedBy": generatedby, - "BIDSVersion": str(bcoin.bidsversion()), + "BIDSVersion": str(bidsversion()), "DatasetType": "raw", "License": "RECOMMENDED. The license for the dataset. The use of license name abbreviations is RECOMMENDED for specifying a license. The corresponding full license text MAY be specified in an additional LICENSE file", "Authors": ["OPTIONAL. List of individuals who contributed to the creation/curation of the dataset"], @@ -97,7 +81,7 @@ def bidscoiner(rawfolder: str, bidsfolder: str, subjects: list=(), force: bool=F readme_file.write_text( f"A free form text ( README ) describing the dataset in more details that SHOULD be provided. For an example, see e.g.:\n" f"https://github.com/bids-standard/bids-starter-kit/blob/main/templates/README.MD\n\n" - f"The raw BIDS data was created using BIDScoin {localversion}\n" + f"The raw BIDS data was created using BIDScoin {__version__}\n" f"All provenance information and settings can be found in ./code/bidscoin\n" f"For more information see: https://github.com/Donders-Institute/bidscoin\n") @@ -355,24 +339,15 @@ def addmetadata(bidsses: Path, subid: str, sesid: str) -> None: def main(): """Console script usage""" + from bidscoin.cli._bidscoiner import get_parser + # Parse the input arguments and run bidscoiner(args) - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=textwrap.dedent(__doc__), - epilog='examples:\n' - ' bidscoiner myproject/raw myproject/bids\n' - ' bidscoiner -f myproject/raw myproject/bids -p sub-009 sub-030\n ') - parser.add_argument('sourcefolder', help='The study root folder containing the raw source data') - parser.add_argument('bidsfolder', help='The destination / output folder with the bids data') - parser.add_argument('-p','--participant_label', help='Space separated list of selected sub-# names / folders to be processed (the sub-prefix can be removed). Otherwise all subjects in the sourcefolder will be selected', nargs='+') - parser.add_argument('-b','--bidsmap', help='The study bidsmap file with the mapping heuristics. If the bidsmap filename is relative (i.e. no "/" in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: bidsmap.yaml', default='bidsmap.yaml') - parser.add_argument('-f','--force', help='Process all subjects, regardless of existing subject folders in the bidsfolder. Otherwise these subject folders will be skipped', action='store_true') - args = parser.parse_args() - - bidscoiner(rawfolder = args.sourcefolder, - bidsfolder = args.bidsfolder, - subjects = args.participant_label, - force = args.force, - bidsmapfile = args.bidsmap) + args = get_parser().parse_args() + bidscoiner(rawfolder = args.sourcefolder, + bidsfolder = args.bidsfolder, + subjects = args.participant_label, + force = args.force, + bidsmapfile = args.bidsmap) if __name__ == "__main__": diff --git a/bidscoin/bidseditor.py b/bidscoin/bidseditor.py index cf7994d6..1719e114 100755 --- a/bidscoin/bidseditor.py +++ b/bidscoin/bidseditor.py @@ -1,16 +1,7 @@ #!/usr/bin/env python3 -""" -This application launches a graphical user interface for editing the bidsmap that is produced -by the bidsmapper. You can edit the BIDS data types and entities until all run-items have a -meaningful and nicely readable BIDS output name. The (saved) bidsmap.yaml output file will be -used by the bidscoiner to do the conversion of the source data to BIDS. - -You can hoover with your mouse over items to get help text (pop-up tooltips). -""" +"""A BIDScoin application with a graphical user interface for editing the bidsmap (See also cli/_bidseditor.py)""" import sys -import argparse -import textwrap import logging import copy import webbrowser @@ -28,11 +19,10 @@ QTreeView, QHBoxLayout, QVBoxLayout, QLabel, QDialog, QMessageBox, QTableWidget, QTableWidgetItem, QHeaderView, QGroupBox, QTextBrowser, QPushButton, QComboBox, QAction) - -try: - from bidscoin import bcoin, bids -except ImportError: - import bcoin, bids # This should work if bidscoin was not pip-installed +from importlib.util import find_spec +if find_spec('bidscoin') is None: + sys.path.append(str(Path(__file__).parents[1])) +from bidscoin import bcoin, bids, bidsversion, version, __version__ ROW_HEIGHT = 22 @@ -40,8 +30,8 @@ BIDSCOIN_ICON = Path(__file__).parent/'bidscoin.ico' RIGHTARROW = Path(__file__).parent/'rightarrow.png' -MAIN_HELP_URL = f"https://bidscoin.readthedocs.io/en/{bcoin.version()}" -HELP_URL_DEFAULT = f"https://bids-specification.readthedocs.io/en/v{bcoin.bidsversion()}" +MAIN_HELP_URL = f"https://bidscoin.readthedocs.io/en/{__version__}" +HELP_URL_DEFAULT = f"https://bids-specification.readthedocs.io/en/v{bidsversion()}" HELP_URLS = { 'anat': f"{HELP_URL_DEFAULT}/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#anatomy-imaging-data", 'dwi' : f"{HELP_URL_DEFAULT}/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#diffusion-imaging-data", @@ -844,10 +834,10 @@ def open_inspectwindow(self, index: int): def show_about(self): """Shows a pop-up window with the BIDScoin version""" - version, message = bcoin.version(check=True) - # QMessageBox.about(self, 'About', f"BIDS editor {version}\n\n{message}") # Has an ugly / small icon image + _, message = version(check=True) + # QMessageBox.about(self, 'About', f"BIDS editor {__version__}\n\n{message}") # Has an ugly / small icon image messagebox = QMessageBox(self) - messagebox.setText(f"\n\nBIDS editor {version}\n\n{message}") + messagebox.setText(f"\n\nBIDS editor {__version__}\n\n{message}") messagebox.setWindowTitle('About') messagebox.setIconPixmap(QtGui.QPixmap(str(BIDSCOIN_LOGO)).scaled(150, 150, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) messagebox.show() @@ -1681,7 +1671,7 @@ def bidseditor(bidsfolder: str, bidsmapfile: str='', templatefile: str='') -> No # Start the Qt-application app = QApplication(sys.argv) - app.setApplicationName(f"{bidsmapfile} - BIDS editor {bcoin.version()}") + app.setApplicationName(f"{bidsmapfile} - BIDS editor {__version__}") mainwin = MainWindow(bidsfolder, input_bidsmap, template_bidsmap, datasaved=True) mainwin.show() app.exec() @@ -1695,20 +1685,10 @@ def bidseditor(bidsfolder: str, bidsmapfile: str='', templatefile: str='') -> No def main(): """Console script usage""" - # Parse the input arguments and run bidseditor - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=textwrap.dedent(__doc__), - epilog=textwrap.dedent(""" - examples: - bidseditor myproject/bids - bidseditor myproject/bids -t bidsmap_dccn.yaml - bidseditor myproject/bids -b my/custom/bidsmap.yaml""")) - - parser.add_argument('bidsfolder', help='The destination folder with the (future) bids data') - parser.add_argument('-b','--bidsmap', help='The study bidsmap file with the mapping heuristics. If the bidsmap filename is relative (i.e. no "/" in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: bidsmap.yaml', default='bidsmap.yaml') - parser.add_argument('-t','--template', help=f'The template bidsmap file with the default heuristics (this could be provided by your institute). If the bidsmap filename is relative (i.e. no "/" in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: {bcoin.bidsmap_template.stem}', default=bcoin.bidsmap_template) - args = parser.parse_args() + from bidscoin.cli._bidseditor import get_parser + # Parse the input arguments and run bidseditor + args = get_parser().parse_args() bidseditor(bidsfolder = args.bidsfolder, bidsmapfile = args.bidsmap, templatefile = args.template) diff --git a/bidscoin/bidsmapper.py b/bidscoin/bidsmapper.py index 1d7abb9f..8dc6e760 100755 --- a/bidscoin/bidsmapper.py +++ b/bidscoin/bidsmapper.py @@ -1,18 +1,7 @@ #!/usr/bin/env python3 -""" -The bidsmapper scans your source data repository to identify different data types by matching -them against the run-items in the template bidsmap. Once a match is found, a mapping to BIDS -output data types is made and the run-item is added to the study bidsmap. You can check and -edit these generated bids-mappings to your needs with the (automatically launched) bidseditor. -Re-run the bidsmapper whenever something was changed in your data acquisition protocol and -edit the new data type to your needs (your existing bidsmap will be re-used). - -The bidsmapper uses plugins, as stored in the bidsmap['Options'], to do the actual work -""" +"""A BIDScoin application to create a study bidsmap (See also cli/_bidsmapper.py)""" # Global imports (plugin modules may be imported when needed) -import argparse -import textwrap import copy import logging import sys @@ -21,12 +10,12 @@ from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: - import bcoin, bids # This should work if bidscoin was not pip-installed +from importlib.util import find_spec +if find_spec('bidscoin') is None: + sys.path.append(str(Path(__file__).parents[1])) +from bidscoin import bcoin, bids, version -localversion, versionmessage = bcoin.version(check=True) +localversion, versionmessage = version(check=True) def bidsmapper(rawfolder: str, bidsfolder: str, bidsmapfile: str, templatefile: str, plugins: list, subprefix: str, sesprefix: str, unzip: str, store: bool=False, noeditor: bool=False, force: bool=False, noupdate: bool=False) -> dict: @@ -241,28 +230,10 @@ def setprefix(bidsmap: dict, subprefix: str, sesprefix: str, rawfolder: Path, up def main(): """Console script usage""" - # Parse the input arguments and run bidsmapper(args) - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=textwrap.dedent(__doc__), - epilog='examples:\n' - ' bidsmapper myproject/raw myproject/bids\n' - ' bidsmapper myproject/raw myproject/bids -t bidsmap_custom # Uses a template bidsmap of choice\n' - ' bidsmapper myproject/raw myproject/bids -p nibabel2bids # Uses a plugin of choice\n' - " bidsmapper myproject/raw myproject/bids -u '*.tar.gz' # Unzip tarball sourcefiles\n ") - parser.add_argument('sourcefolder', help='The study root folder containing the raw source data folders') - parser.add_argument('bidsfolder', help='The destination folder with the (future) bids data and the bidsfolder/code/bidscoin/bidsmap.yaml output file') - parser.add_argument('-b','--bidsmap', help="The study bidsmap file with the mapping heuristics. If the bidsmap filename is relative (i.e. no '/' in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: bidsmap.yaml", default='bidsmap.yaml') - parser.add_argument('-t','--template', help=f"The bidsmap template file with the default heuristics (this could be provided by your institute). If the bidsmap filename is relative (i.e. no '/' in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: {bcoin.bidsmap_template.stem}", default=bcoin.bidsmap_template) - parser.add_argument('-p','--plugins', help='List of plugins to be used. Default: the plugin list of the study/template bidsmap)', nargs='+', default=[]) - parser.add_argument('-n','--subprefix', help="The prefix common for all the source subject-folders (e.g. 'Pt' is the subprefix if subject folders are named 'Pt018', 'Pt019', ...). Use '*' when your subject folders do not have a prefix. Default: the value of the study/template bidsmap, e.g. 'sub-'") - parser.add_argument('-m','--sesprefix', help="The prefix common for all the source session-folders (e.g. 'M_' is the subprefix if session folders are named 'M_pre', 'M_post', ..). Use '*' when your session folders do not have a prefix. Default: the value of the study/template bidsmap, e.g. 'ses-'") - parser.add_argument('-u','--unzip', help='Wildcard pattern to unpack tarball/zip-files in the sub/ses sourcefolder that need to be unzipped (in a tempdir) to make the data readable. Default: the value of the study/template bidsmap') - parser.add_argument('-s','--store', help='Store provenance data samples in the bidsfolder/code/provenance folder (useful for inspecting e.g. zipped or transfered datasets)', action='store_true') - parser.add_argument('-a','--automated', help='Save the automatically generated bidsmap to disk and without interactively tweaking it with the bidseditor', action='store_true') - parser.add_argument('-f','--force', help='Discard the previously saved bidsmap and logfile', action='store_true') - parser.add_argument('--no-update', help="Do not update any sub/sesprefixes in or prepend the sourcefolder name to the <> expression that extracts the subject/session labels. This is normally done to make the extraction more robust, but could case problems for certain use cases", action='store_true') - args = parser.parse_args() + from bidscoin.cli._bidsmapper import get_parser + # Parse the input arguments and run bidsmapper(args) + args = get_parser().parse_args() bidsmapper(rawfolder = args.sourcefolder, bidsfolder = args.bidsfolder, bidsmapfile = args.bidsmap, diff --git a/bidscoin/cli/__init__.py b/bidscoin/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bidscoin/cli/_bcoin.py b/bidscoin/cli/_bcoin.py new file mode 100755 index 00000000..02b9dd23 --- /dev/null +++ b/bidscoin/cli/_bcoin.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +""" +BIDScoin is a toolkit to convert and organize raw data-sets according to the Brain Imaging Data Structure (BIDS) + +The basic workflow is to run these two tools: + + $ bidsmapper sourcefolder bidsfolder # This produces a study bidsmap and launches a GUI + $ bidscoiner sourcefolder bidsfolder # This converts your data to BIDS according to the study bidsmap + +Set the environment variable BIDSCOIN_DEBUG=TRUE in your console to run BIDScoin in its more verbose DEBUG logging mode + +For more documentation see: https://bidscoin.readthedocs.io +""" + +import argparse +import textwrap +from importlib.util import find_spec +if find_spec('bidscoin') is None: + import sys + from pathlib import Path + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import version, bidsversion, bidsmap_template + + +def get_parser() -> argparse.ArgumentParser: + """Build an argument parser with input arguments for bcoin.py""" + + localversion, versionmessage = version(check=True) + + parser = argparse.ArgumentParser(prog='bidscoin', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(__doc__), + epilog='examples:\n' + ' bidscoin -l\n' + ' bidscoin -d data/bidscoin_tutorial\n' + ' bidscoin -t\n' + ' bidscoin -t my_template_bidsmap\n' + ' bidscoin -b my_study_bidsmap\n' + ' bidscoin -i data/my_template_bidsmap.yaml downloads/my_plugin.py\n ') + parser.add_argument('-l', '--list', help='List all executables (i.e. the apps, bidsapps and utilities)', action='store_true') + parser.add_argument('-p', '--plugins', help='List all installed plugins and template bidsmaps', action='store_true') + parser.add_argument('-i', '--install', help='A list of template bidsmaps and/or bidscoin plugins to install', nargs='+') + parser.add_argument('-u', '--uninstall', help='A list of template bidsmaps and/or bidscoin plugins to uninstall', nargs='+') + parser.add_argument('-d', '--download', help='Download folder. If given, tutorial MRI data will be downloaded here') + parser.add_argument('-t', '--test', help='Test the bidscoin installation and template bidsmap', nargs='?', const=bidsmap_template) + parser.add_argument('-b', '--bidsmaptest', help='Test the run-items and their bidsnames of all normal runs in the study bidsmap. Provide the bids-folder or the bidsmap filepath') + parser.add_argument('-v', '--version', help='Show the installed version and check for updates', action='version', version=f"BIDS-version:\t\t{bidsversion()}\nBIDScoin-version:\t{localversion}, {versionmessage}") + + return parser diff --git a/bidscoin/cli/_bidscoiner.py b/bidscoin/cli/_bidscoiner.py new file mode 100755 index 00000000..6794c7ad --- /dev/null +++ b/bidscoin/cli/_bidscoiner.py @@ -0,0 +1,35 @@ +""" +Converts ("coins") your source datasets to NIfTI/json/tsv BIDS datasets using the mapping +information from the bidsmap.yaml file. Edit this bidsmap to your needs using the bidseditor +tool before running this function or (re-)run the bidsmapper whenever you encounter unexpected +data. You can run bidscoiner after all data has been collected, or run / re-run it whenever +new data has been added to your source folder (presuming the scan protocol hasn't changed). +Also, if you delete a subject/session folder from the bidsfolder, it will simply be re-created +from the sourcefolder the next time you run the bidscoiner. + +The bidscoiner uses plugins, as stored in the bidsmap['Options'], to do the actual work + +Provenance information, warnings and error messages are stored in the +bidsfolder/code/bidscoin/bidscoiner.log file. +""" + +import argparse +import textwrap + + +def get_parser(): + """Build an argument parser with input arguments for bidscoiner.py""" + + parser = argparse.ArgumentParser(prog='bidscoiner', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(__doc__), + epilog='examples:\n' + ' bidscoiner myproject/raw myproject/bids\n' + ' bidscoiner -f myproject/raw myproject/bids -p sub-009 sub-030\n ') + parser.add_argument('sourcefolder', help='The study root folder containing the raw source data') + parser.add_argument('bidsfolder', help='The destination / output folder with the bids data') + parser.add_argument('-p','--participant_label', help='Space separated list of selected sub-# names / folders to be processed (the sub-prefix can be removed). Otherwise all subjects in the sourcefolder will be selected', nargs='+') + parser.add_argument('-b','--bidsmap', help='The study bidsmap file with the mapping heuristics. If the bidsmap filename is relative (i.e. no "/" in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: bidsmap.yaml', default='bidsmap.yaml') + parser.add_argument('-f','--force', help='Process all subjects, regardless of existing subject folders in the bidsfolder. Otherwise these subject folders will be skipped', action='store_true') + + return parser diff --git a/bidscoin/cli/_bidseditor.py b/bidscoin/cli/_bidseditor.py new file mode 100755 index 00000000..c2386a0c --- /dev/null +++ b/bidscoin/cli/_bidseditor.py @@ -0,0 +1,36 @@ +""" +This application launches a graphical user interface for editing the bidsmap that is produced +by the bidsmapper. You can edit the BIDS data types and entities until all run-items have a +meaningful and nicely readable BIDS output name. The (saved) bidsmap.yaml output file will be +used by the bidscoiner to do the conversion of the source data to BIDS. + +You can hoover with your mouse over items to get help text (pop-up tooltips). +""" + +import argparse +import textwrap +from importlib.util import find_spec +if find_spec('bidscoin') is None: + import sys + from pathlib import Path + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bidsmap_template + + +def get_parser() -> argparse.ArgumentParser: + """Build an argument parser with input arguments for bidseditor.py""" + + parser = argparse.ArgumentParser(prog='bidseditor', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(__doc__), + epilog=textwrap.dedent(""" + examples: + bidseditor myproject/bids + bidseditor myproject/bids -t bidsmap_dccn.yaml + bidseditor myproject/bids -b my/custom/bidsmap.yaml""")) + + parser.add_argument('bidsfolder', help='The destination folder with the (future) bids data') + parser.add_argument('-b','--bidsmap', help='The study bidsmap file with the mapping heuristics. If the bidsmap filename is relative (i.e. no "/" in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: bidsmap.yaml', default='bidsmap.yaml') + parser.add_argument('-t','--template', help=f'The template bidsmap file with the default heuristics (this could be provided by your institute). If the bidsmap filename is relative (i.e. no "/" in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: {bidsmap_template.stem}', default=bidsmap_template) + + return parser diff --git a/bidscoin/cli/_bidsmapper.py b/bidscoin/cli/_bidsmapper.py new file mode 100755 index 00000000..06635242 --- /dev/null +++ b/bidscoin/cli/_bidsmapper.py @@ -0,0 +1,46 @@ +""" +The bidsmapper scans your source data repository to identify different data types by matching +them against the run-items in the template bidsmap. Once a match is found, a mapping to BIDS +output data types is made and the run-item is added to the study bidsmap. You can check and +edit these generated bids-mappings to your needs with the (automatically launched) bidseditor. +Re-run the bidsmapper whenever something was changed in your data acquisition protocol and +edit the new data type to your needs (your existing bidsmap will be re-used). + +The bidsmapper uses plugins, as stored in the bidsmap['Options'], to do the actual work +""" + +import argparse +import textwrap +from importlib.util import find_spec +if find_spec('bidscoin') is None: + import sys + from pathlib import Path + sys.path.append(str(Path(__file__).parents[1])) +from bidscoin import bidsmap_template + + +def get_parser() -> argparse.ArgumentParser: + """Build an argument parser with input arguments for bidsmapper.py""" + + parser = argparse.ArgumentParser(prog='bidsmapper', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(__doc__), + epilog='examples:\n' + ' bidsmapper myproject/raw myproject/bids\n' + ' bidsmapper myproject/raw myproject/bids -t bidsmap_custom # Uses a template bidsmap of choice\n' + ' bidsmapper myproject/raw myproject/bids -p nibabel2bids # Uses a plugin of choice\n' + " bidsmapper myproject/raw myproject/bids -u '*.tar.gz' # Unzip tarball sourcefiles\n ") + parser.add_argument('sourcefolder', help='The study root folder containing the raw source data folders') + parser.add_argument('bidsfolder', help='The destination folder with the (future) bids data and the bidsfolder/code/bidscoin/bidsmap.yaml output file') + parser.add_argument('-b','--bidsmap', help="The study bidsmap file with the mapping heuristics. If the bidsmap filename is relative (i.e. no '/' in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: bidsmap.yaml", default='bidsmap.yaml') + parser.add_argument('-t','--template', help=f"The bidsmap template file with the default heuristics (this could be provided by your institute). If the bidsmap filename is relative (i.e. no '/' in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: {bidsmap_template.stem}", default=bidsmap_template) + parser.add_argument('-p','--plugins', help='List of plugins to be used. Default: the plugin list of the study/template bidsmap)', nargs='+', default=[]) + parser.add_argument('-n','--subprefix', help="The prefix common for all the source subject-folders (e.g. 'Pt' is the subprefix if subject folders are named 'Pt018', 'Pt019', ...). Use '*' when your subject folders do not have a prefix. Default: the value of the study/template bidsmap, e.g. 'sub-'") + parser.add_argument('-m','--sesprefix', help="The prefix common for all the source session-folders (e.g. 'M_' is the subprefix if session folders are named 'M_pre', 'M_post', ..). Use '*' when your session folders do not have a prefix. Default: the value of the study/template bidsmap, e.g. 'ses-'") + parser.add_argument('-u','--unzip', help='Wildcard pattern to unpack tarball/zip-files in the sub/ses sourcefolder that need to be unzipped (in a tempdir) to make the data readable. Default: the value of the study/template bidsmap') + parser.add_argument('-s','--store', help='Store provenance data samples in the bidsfolder/code/provenance folder (useful for inspecting e.g. zipped or transfered datasets)', action='store_true') + parser.add_argument('-a','--automated', help='Save the automatically generated bidsmap to disk and without interactively tweaking it with the bidseditor', action='store_true') + parser.add_argument('-f','--force', help='Discard the previously saved bidsmap and logfile', action='store_true') + parser.add_argument('--no-update', help="Do not update any sub/sesprefixes in or prepend the sourcefolder name to the <> expression that extracts the subject/session labels. This is normally done to make the extraction more robust, but could case problems for certain use cases", action='store_true') + + return parser diff --git a/bidscoin/cli/_bidsparticipants.py b/bidscoin/cli/_bidsparticipants.py new file mode 100755 index 00000000..9a04cefe --- /dev/null +++ b/bidscoin/cli/_bidsparticipants.py @@ -0,0 +1,30 @@ +""" +(Re)scans data sets in the source folder for subject metadata to populate the participants.tsv +file in the bids directory, e.g. after you renamed (be careful there!), added or deleted data +in the bids folder yourself. + +Provenance information, warnings and error messages are stored in the +bidsfolder/code/bidscoin/bidsparticipants.log file. +""" + +import argparse +import textwrap + + +def get_parser(): + """Build an argument parser with input arguments for bidsparticipants.py""" + + # Parse the input arguments and run bidsparticipants(args) + parser = argparse.ArgumentParser(prog='bidsparticipants', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(__doc__), + epilog='examples:\n' + ' bidsparticipants myproject/raw myproject/bids\n' + ' bidsparticipants myproject/raw myproject/bids -k participant_id age sex\n ') + parser.add_argument('sourcefolder', help='The study root folder containing the raw source data folders') + parser.add_argument('bidsfolder', help='The destination / output folder with the bids data') + parser.add_argument('-k','--keys', help="Space separated list of the participants.tsv columns. Default: 'session_id' 'age' 'sex' 'size' 'weight'", nargs='+', default=['age', 'sex', 'size', 'weight']) # NB: session_id is default + parser.add_argument('-d','--dryrun', help='Do not save anything, only print the participants info on screen', action='store_true') + parser.add_argument('-b','--bidsmap', help='The study bidsmap file with the mapping heuristics. If the bidsmap filename is relative (i.e. no "/" in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: bidsmap.yaml', default='bidsmap.yaml') + + return parser diff --git a/bidscoin/cli/_deface.py b/bidscoin/cli/_deface.py new file mode 100755 index 00000000..90e7a98a --- /dev/null +++ b/bidscoin/cli/_deface.py @@ -0,0 +1,39 @@ +""" +A wrapper around the 'pydeface' defacing tool (https://github.com/poldracklab/pydeface). + +Except for BIDS inheritances and IntendedFor usage, this wrapper is BIDS-aware (a 'bidsapp') +and writes BIDS compliant output + +Linux users can distribute the computations to their HPC compute cluster if the DRMAA +libraries are installed and the DRMAA_LIBRARY_PATH environment variable set + +For multi-echo data see `medeface` +""" + +import argparse +import json + + +def get_parser(): + """Build an argument parser with input arguments for deface.py""" + + class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass + + parser = argparse.ArgumentParser(prog='deface', + formatter_class=CustomFormatter, + description=__doc__, + epilog='examples:\n' + ' deface myproject/bids anat/*_T1w*\n' + ' deface myproject/bids anat/*_T1w* -p 001 003 -o derivatives\n' + ' deface myproject/bids anat/*_T1w* -c -n "-l walltime=00:60:00,mem=4gb"\n' + ' deface myproject/bids anat/*_T1w* -a \'{"cost": "corratio", "verbose": ""}\'\n ') + parser.add_argument('bidsfolder', help='The bids-directory with the subject data') + parser.add_argument('pattern', help="Globlike search pattern (relative to the subject/session folder) to select the images that need to be defaced, e.g. 'anat/*_T1w*'") + parser.add_argument('-p','--participant_label', help='Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed', nargs='+') + parser.add_argument('-o','--output', help=f"A string that determines where the defaced images are saved. It can be the name of a BIDS datatype folder, such as 'anat', or of the derivatives folder, i.e. 'derivatives'. If output is left empty then the original images are replaced by the defaced images") + parser.add_argument('-c','--cluster', help='Use the DRMAA library to submit the deface jobs to a high-performance compute (HPC) cluster', action='store_true') + parser.add_argument('-n','--nativespec', help='DRMAA native specifications for submitting deface jobs to the HPC cluster', default='-l walltime=00:30:00,mem=2gb') + parser.add_argument('-a','--args', help='Additional arguments (in dict/json-style) that are passed to pydeface. See examples for usage', type=json.loads, default={}) + parser.add_argument('-f','--force', help='Deface all images, regardless if they have already been defaced (i.e. if {"Defaced": True} in the json sidecar file)', action='store_true') + + return parser diff --git a/bidscoin/cli/_dicomsort.py b/bidscoin/cli/_dicomsort.py new file mode 100755 index 00000000..78d5d080 --- /dev/null +++ b/bidscoin/cli/_dicomsort.py @@ -0,0 +1,33 @@ +""" +Sorts and/or renames DICOM files into local subfolders, e.g. with 3-digit SeriesNumber-SeriesDescription +folder names (i.e. following the same listing as on the scanner console) + +Supports flat DICOM as well as multi-subject/session DICOMDIR file structures. +""" + +import argparse +import textwrap + + +def get_parser(): + """Build an argument parser with input arguments for dicomsort.py""" + + class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass + + parser = argparse.ArgumentParser(prog='dicomsort', + formatter_class=CustomFormatter, + description=textwrap.dedent(__doc__), + epilog='examples:\n' + ' dicomsort sub-011/ses-mri01\n' + ' dicomsort sub-011/ses-mri01/DICOMDIR -n {AcquisitionNumber:05d}_{InstanceNumber:05d}.dcm\n' + ' dicomsort myproject/raw/DICOMDIR --subprefix pat^ --sesprefix\n ') + parser.add_argument('dicomsource', help='The root folder containing the dicomsource/[sub/][ses/] dicomfiles or the DICOMDIR file') + parser.add_argument('-i','--subprefix', help='Provide a prefix string for recursive sorting of dicomsource/subject subfolders (e.g. "sub-")') + parser.add_argument('-j','--sesprefix', help='Provide a prefix string for recursive sorting of dicomsource/subject/session subfolders (e.g. "ses-")') + parser.add_argument('-f','--folderscheme', help='Naming scheme for the sorted DICOM Series subfolders. Follows the Python string formatting syntax with DICOM field names in curly bracers with an optional number of digits for numeric fields. Sorting in subfolders is skipped when an empty folderscheme is given (but note that renaming the filenames can still be performed)', default='{SeriesNumber:03d}-{SeriesDescription}') + parser.add_argument('-n','--namescheme', help='Optional naming scheme that can be provided to rename the DICOM files. Follows the Python string formatting syntax with DICOM field names in curly bracers with an optional number of digits for numeric fields. Use e.g. "{PatientName}_{SeriesNumber:03d}_{SeriesDescription}_{AcquisitionNumber:05d}_{InstanceNumber:05d}.dcm" or "{InstanceNumber:05d}_{SOPInstanceUID}.IMA" for default names') + parser.add_argument('-p','--pattern', help='The regular expression pattern used in re.match(pattern, dicomfile) to select the dicom files', default=r'.*\.(IMA|dcm)$') + parser.add_argument('--force', help='Sort the DICOM data even the DICOM fields of the folder/name scheme are not in the data', action='store_true') + parser.add_argument('-d','--dryrun', help='Only print the dicomsort commands without actually doing anything', action='store_true') + + return parser diff --git a/bidscoin/cli/_echocombine.py b/bidscoin/cli/_echocombine.py new file mode 100755 index 00000000..7ad1b514 --- /dev/null +++ b/bidscoin/cli/_echocombine.py @@ -0,0 +1,34 @@ +""" +A wrapper around the 'mecombine' multi-echo combination tool +(https://github.com/Donders-Institute/multiecho). + +Except for BIDS inheritances, this wrapper is BIDS-aware (a 'bidsapp') and writes BIDS +compliant output +""" + +import argparse + + +def get_parser(): + """Build an argument parser with input arguments for echocombine.py""" + + class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass + + parser = argparse.ArgumentParser(prog='echocombine', + formatter_class=CustomFormatter, + description=__doc__, + epilog='examples:\n' + ' echocombine myproject/bids func/*task-stroop*echo-1*\n' + ' echocombine myproject/bids *task-stroop*echo-1* -p 001 003\n' + ' echocombine myproject/bids func/*task-*echo-1* -o func\n' + ' echocombine myproject/bids func/*task-*echo-1* -o derivatives -w 13 26 39 52\n' + ' echocombine myproject/bids func/*task-*echo-1* -a PAID\n ') + parser.add_argument('bidsfolder', help='The bids-directory with the (multi-echo) subject data') + parser.add_argument('pattern', help="Globlike recursive search pattern (relative to the subject/session folder) to select the first echo of the images that need to be combined, e.g. '*task-*echo-1*'") + parser.add_argument('-p','--participant_label', help='Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed', nargs='+') + parser.add_argument('-o','--output', help=f"A string that determines where the output is saved. It can be the name of a BIDS datatype folder, such as 'func', or of the derivatives folder, i.e. 'derivatives'. If output = [the name of the input datatype folder] then the original echo images are replaced by one combined image. If output is left empty then the combined image is saved in the input datatype folder and the original echo images are moved to the 'extra_data' folder", default='') + parser.add_argument('-a','--algorithm', help='Combination algorithm', choices=['PAID', 'TE', 'average'], default='TE') + parser.add_argument('-w','--weights', help='Weights for each echo', nargs='*', default=None, type=list) + parser.add_argument('-f','--force', help='Process all images, regardless whether target images already exist. Otherwise the echo-combination will be skipped', action='store_true') + + return parser diff --git a/bidscoin/cli/_medeface.py b/bidscoin/cli/_medeface.py new file mode 100755 index 00000000..bfc13027 --- /dev/null +++ b/bidscoin/cli/_medeface.py @@ -0,0 +1,43 @@ +""" +A wrapper around the 'pydeface' defacing tool (https://github.com/poldracklab/pydeface) that +computes a defacing mask on a (temporary) echo-combined image and then applies it to each +individual echo-image. + +Except for BIDS inheritances and IntendedFor usage, this wrapper is BIDS-aware (a 'bidsapp') +and writes BIDS compliant output + +Linux users can distribute the computations to their HPC compute cluster if the DRMAA +libraries are installed and the DRMAA_LIBRARY_PATH environment variable set + +For single-echo data see `deface` +""" + +import argparse +import json + + +def get_parser(): + """Build an argument parser with input arguments for medeface.py""" + + class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass + + parser = argparse.ArgumentParser(prog='medeface', + formatter_class=CustomFormatter, + description=__doc__, + epilog='examples:\n' + ' medeface myproject/bids anat/*_T1w*\n' + ' medeface myproject/bids anat/*_T1w* -p 001 003 -o derivatives\n' + ' medeface myproject/bids anat/*_T1w* -c -n "-l walltime=00:60:00,mem=4gb"\n' + ' medeface myproject/bids anat/*acq-GRE* -m anat/*acq-GRE*magnitude*"\n' + ' medeface myproject/bids anat/*_FLAIR* -a \'{"cost": "corratio", "verbose": ""}\'\n ') + parser.add_argument('bidsfolder', help='The bids-directory with the (multi-echo) subject data') + parser.add_argument('pattern', help="Globlike search pattern (relative to the subject/session folder) to select the images that need to be defaced, e.g. 'anat/*_T2starw*'") + parser.add_argument('-m','--maskpattern', help="Globlike search pattern (relative to the subject/session folder) to select the images from which the defacemask is computed, e.g. 'anat/*_part-mag_*_T2starw*'. If not given then 'pattern' is used") + parser.add_argument('-p','--participant_label', help='Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed', nargs='+') + parser.add_argument('-o','--output', help=f"A string that determines where the defaced images are saved. It can be the name of a BIDS datatype folder, such as 'anat', or of the derivatives folder, i.e. 'derivatives'. If output is left empty then the original images are replaced by the defaced images") + parser.add_argument('-c','--cluster', help='Submit the deface jobs to a high-performance compute (HPC) cluster', action='store_true') + parser.add_argument('-n','--nativespec', help='DRMAA native specifications for submitting deface jobs to the HPC cluster', default='-l walltime=00:30:00,mem=2gb') + parser.add_argument('-a','--args', help='Additional arguments (in dict/json-style) that are passed to pydeface. See examples for usage', type=json.loads, default={}) + parser.add_argument('-f','--force', help='Process all images, regardless if images have already been defaced (i.e. if {"Defaced": True} in the json sidecar file)', action='store_true') + + return parser diff --git a/bidscoin/cli/_physio2tsv.py b/bidscoin/cli/_physio2tsv.py new file mode 100755 index 00000000..16586b75 --- /dev/null +++ b/bidscoin/cli/_physio2tsv.py @@ -0,0 +1,25 @@ +""" +Reads and writes active (i.e. non-zero) signals from SIEMENS advanced physiological log / DICOM files + +This function expects to find either a combination of individual logfiles (*_ECG.log, *_RESP.log, +*_PULS.log, *_EXT.log, *_Info.log) generated by >=R013 sequences, or a single encoded "_PHYSIO" DICOM +file generated by >=R015 sequences. +""" + +import argparse + + +def get_parser(): + """Build an argument parser with input arguments for physio2tsv.py""" + + parser = argparse.ArgumentParser(prog='physio2tsv', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__, + epilog='examples:\n' + ' physio2tsv myproject/sub-001/MR000000.dcm myproject/bids/sub-001/func/sub-001_physio\n' + ' physio2tsv myproject/sub-001/Physio_20200428_142451_007e910e-02d9-4d7a-8fdb-8e3568be8322 myproject/bids/sub-001/func/sub-001_physio\n\n' + '@author: Marcel Zwiers\n ') + parser.add_argument('physiofile', help="Either the fullpath of the DICOM file or the basename of the PHYSIO logfiles (fullpath without suffix and file extension, e.g. 'foo/bar/Physio_DATE_TIME_UUID'") + parser.add_argument('tsvfile', help="The fullpath of the BIDS filenames, e.g. 'foo/bids/sub-001/func/sub-001_physio'") + + return parser diff --git a/bidscoin/cli/_plotphysio.py b/bidscoin/cli/_plotphysio.py new file mode 100755 index 00000000..a7682e68 --- /dev/null +++ b/bidscoin/cli/_plotphysio.py @@ -0,0 +1,25 @@ +""" +Reads and plots active (i.e. non-zero) signals from SIEMENS advanced physiological log / DICOM files + +This function expects to find either a combination of individual logfiles (*_ECG.log, *_RESP.log, +*_PULS.log, *_EXT.log, *_Info.log) generated by >=R013 sequences, or a single encoded "_PHYSIO" DICOM +file generated by >=R015 sequences. +""" + +import argparse + + +def get_parser(): + """Build an argument parser with input arguments for plotphysio.py""" + + parser = argparse.ArgumentParser(prog='plotphysio', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__, + epilog='examples:\n' + ' plotphysio myproject/sub-001/MR000000.dcm\n' + ' plotphysio -s 2000 myproject/sub-001/Physio_20200428_142451_007e910e-02d9-4d7a-8fdb-8e3568be8322\n\n' + '@author: Marcel Zwiers\n ') + parser.add_argument('filename', help="Either the fullpath of the DICOM file or the basename of the PHYSIO logfiles (fullpath without suffix and file extension, e.g. 'foo/bar/Physio_DATE_TIME_UUID'") + parser.add_argument('-s','--showsamples', help='The nr of plotted samples of the physiological traces (default: 1000, nothing is plotted if 0)', default=1000, type=int) + + return parser diff --git a/bidscoin/cli/_rawmapper.py b/bidscoin/cli/_rawmapper.py new file mode 100755 index 00000000..a98f3d1b --- /dev/null +++ b/bidscoin/cli/_rawmapper.py @@ -0,0 +1,39 @@ +""" +Maps out the values of a dicom attribute of all subjects in the sourcefolder, saves the result +in a mapper-file and, optionally, uses the dicom values to rename the sub-/ses-id's of the +subfolders. This latter option can be used, e.g. when an alternative subject id was entered in +the [Additional info] field during subject registration at the scanner console (i.e. this data +is stored in the dicom attribute named 'PatientComments') +""" + +import argparse +import textwrap + + +def get_parser(): + """Build an argument parser with input arguments for rawmapper.py""" + + class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass + + parser = argparse.ArgumentParser(prog='rawmapper', + formatter_class=CustomFormatter, + description=textwrap.dedent(__doc__), + epilog='examples:\n' + ' rawmapper myproject/raw\n' + ' rawmapper myproject/raw -f AcquisitionDate\n' + ' rawmapper myproject/raw -s sub-100/ses-mri01 sub-126/ses-mri01\n' + ' rawmapper myproject/raw -r -f ManufacturerModelName AcquisitionDate --dryrun\n' + ' rawmapper myproject/raw -r -s sub-1*/* sub-2*/ses-mri01 --dryrun\n' + ' rawmapper -f EchoTime -w *fMRI* myproject/raw\n ') + parser.add_argument('sourcefolder', help='The source folder with the raw data in sub-#/ses-#/series organisation') + parser.add_argument('-s','--sessions', help='Space separated list of selected sub-#/ses-# names / folders to be processed. Otherwise all sessions in the bidsfolder will be selected', nargs='+') + parser.add_argument('-f','--field', help='The fieldname(s) of the dicom attribute(s) used to rename or map the subid/sesid foldernames', default=['PatientComments', 'ImageComments'], nargs='+') + parser.add_argument('-w','--wildcard', help='The Unix style pathname pattern expansion that is used to select the series from which the dicomfield is being mapped (can contain wildcards)', default='*') + parser.add_argument('-o','--outfolder', help='The mapper-file is normally saved in sourcefolder or, when using this option, in outfolder') + parser.add_argument('-r','--rename', help='Rename sub-subid/ses-sesid directories in the sourcefolder to sub-dcmval/ses-dcmval', action='store_true') + parser.add_argument('-c','--clobber', help='Rename the sub/ses directories, even if the target-directory already exists', action='store_true') + parser.add_argument('-n','--subprefix', help="The prefix common for all the source subject-folders. Use a '*' wildcard if there is no prefix", default='sub-') + parser.add_argument('-m','--sesprefix', help="The prefix common for all the source session-folders. Use a '*' wildcard if there is no prefix or an empty value if there are no sessions", nargs='?', default='ses-') + parser.add_argument('-d','--dryrun', help='Dryrun (test) the mapping or renaming of the sub-subid/ses-sesid directories (i.e. nothing is stored on disk and directory names are not actually changed))', action='store_true') + + return parser diff --git a/bidscoin/cli/_skullstrip.py b/bidscoin/cli/_skullstrip.py new file mode 100755 index 00000000..1a1ef2df --- /dev/null +++ b/bidscoin/cli/_skullstrip.py @@ -0,0 +1,36 @@ +""" +A wrapper around FreeSurfer's 'synthstrip' skull stripping tool +(https://surfer.nmr.mgh.harvard.edu/docs/synthstrip). Except for BIDS inheritances, +this wrapper is BIDS-aware (a 'bidsapp') and writes BIDS compliant output + +The corresponding brain mask is saved in the bids/derivatives/synthstrip folder + +Assumes the installation of FreeSurfer v7.3.2 or higher +""" + +import argparse + + +def get_parser(): + """Build an argument parser with input arguments for skullstrip.py""" + + class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass + + parser = argparse.ArgumentParser(prog='skullstrip', + formatter_class=CustomFormatter, + description=__doc__, + epilog='examples:\n' + ' skullstrip myproject/bids anat/*_T1w*\n' + ' skullstrip myproject/bids anat/*_T1w* -p 001 003 -a \' --no-csf\'\n' + ' skullstrip myproject/bids fmap/*_magnitude1* -m fmap/*_phasediff* -o extra_data fmap\n' + ' skullstrip myproject/bids fmap/*_acq-mylabel*_magnitude1* -m fmap/*_acq-mylabel_* -o fmap\n ') + parser.add_argument('bidsfolder', help="The bids-directory with the subject data", type=str) + parser.add_argument('pattern', help="Globlike search pattern (relative to the subject/session folder) to select the (3D) images that need to be skullstripped, e.g. 'anat/*_T1w*'", type=str) + parser.add_argument('-p','--participant_label', help="Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed", type=str, nargs='+') + parser.add_argument('-m','--masked', help="Globlike search pattern (relative to the subject/session folder) to select additional (3D/4D) images from the same space that need to be masked with the same mask, e.g. 'fmap/*_phasediff'. NB: This option can only be used if pattern yields a single file per session", type=str) + parser.add_argument('-o','--output', help="One or two output strings that determine where the skullstripped + additional masked images are saved. Each output string can be the name of a BIDS datatype folder, such as 'anat', or of the derivatives folder, i.e. 'derivatives' (default). If the output string is the same as the datatype then the original images are replaced by the skullstripped images", nargs='+') + parser.add_argument('-f','--force', help="Process images, regardless whether images have already been skullstripped (i.e. if {'SkullStripped': True} in the json sidecar file)", action='store_true') + parser.add_argument('-a','--args', help="Additional arguments that are passed to synthstrip (NB: Use quotes and a leading space to prevent unintended argument parsing)", type=str, default='') + parser.add_argument('-c','--cluster', help='Use `qsub` to submit the skullstrip jobs to a high-performance compute (HPC) cluster. Can only be used if `--masked` is left empty', action='store_true') + + return parser diff --git a/bidscoin/cli/_slicereport.py b/bidscoin/cli/_slicereport.py new file mode 100755 index 00000000..bbd61306 --- /dev/null +++ b/bidscoin/cli/_slicereport.py @@ -0,0 +1,64 @@ +""" +A wrapper around the 'slicer' imaging tool (https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/Miscvis) +to generate a web page with a row of image slices for each subject in the BIDS repository, as +well as individual sub-pages displaying more detailed information. The input images are +selectable using wildcards, and the output images are configurable via various user options, +allowing you to quickly create a custom 'slicer' report to do visual quality control on any +datatype in your repository. + +Requires an existing installation of FSL/slicer +""" + +import argparse + + +def get_parser(): + """Build an argument parser with input arguments for slicereport.py""" + + epilogue = """ +OPTIONS: + L : Label slices with slice number. + l [LUT] : Use a different colour map from that specified in the header. + i [MIN] [MAX] : Specify intensity min and max for display range. + e [THR] : Use the specified threshold for edges (if > 0 use this proportion of max-min, + if < 0, use the absolute value) + t : Produce semi-transparent (dithered) edges. + n : Use nearest-neighbour interpolation for output. + u : Do not put left-right labels in output. + s : Size scaling factor + c : Add a red dot marker to top right of image + +OUTPUTS: + x/y/z [SLICE] [..] : Output sagittal, coronal or axial slice (if [SLICE] > 0 it is a + fraction of image dimension, if < 0, it is an absolute slice number) + a : Output mid-sagittal, -coronal and -axial slices into one image + A [WIDTH] : Output _all_ axial slices into one image of _max_ width [WIDTH] + S [SAMPLE] [WIDTH] : As `A` but only include every [SAMPLE]'th slice + LF : Start a new line (i.e. works like a row break) + +examples: + slicereport myproject/bids anat/*_T1w* + slicereport myproject/bids anat/*_T2w* -r myproject/QC/slicereport_T2 -x myproject/QC/slicereport_T1 + slicereport myproject/bids fmap/*_phasediff* -o fmap/*_magnitude1* + slicereport myproject/bids/derivatives/fmriprep anat/*run-?_desc-preproc_T1w* -o anat/*run-?_label-GM* + slicereport myproject/bids/derivatives/deface anat/*_T1w* -o myproject/bids:anat/*_T1w* --options L e 0.05 + slicereport myproject/bids anat/*_T1w* --outputs x 0.3 x 0.4 x 0.5 x 0.6 x 0.7 LF z 0.3 z 0.4 z 0.5 z 0.6 z 0.7\n """ + + parser = argparse.ArgumentParser(prog='slicereport', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__, epilog=epilogue) + parser.add_argument('bidsfolder', help='The bids-directory with the subject data') + parser.add_argument('pattern', help="Globlike search pattern to select the images in bidsfolder to be reported, e.g. 'anat/*_T2starw*'") + parser.add_argument('-o','--outlinepattern', help="Globlike search pattern to select red outline images that are projected on top of the reported images (i.e. 'outlinepattern' must yield the same number of images as 'pattern'. Prepend `outlinedir:` if your outline images are in `outlinedir` instead of `bidsdir` (see examples below)`") + parser.add_argument('-i','--outlineimage', help='A common red-outline image that is projected on top of all images', default='') + parser.add_argument('-p','--participant_label', help='Space separated list of sub-# identifiers to be processed (the sub-prefix can be left out). If not specified then all sub-folders in the bidsfolder will be processed', nargs='+') + parser.add_argument('-r','--reportfolder', help="The folder where the report is saved (default: bidsfolder/derivatives/slicereport)") + parser.add_argument('-x','--xlinkfolder', help="A (list of) QC report folder(s) with cross-linkable sub-reports, e.g. bidsfolder/derivatives/mriqc", nargs='+') + parser.add_argument('-q','--qcscores', help="Column names for creating an accompanying tsv-file to store QC-rating scores (default: rating_overall)", default=['rating_overall'], nargs='+') + parser.add_argument('-c','--cluster', help='Use `qsub` to submit the slicer jobs to a high-performance compute (HPC) cluster', action='store_true') + parser.add_argument('--options', help='Main options of slicer (see below). (default: "s 1")', default=['s','1'], nargs='+') + parser.add_argument('--outputs', help='Output options of slicer (see below). (default: "x 0.4 x 0.5 x 0.6 y 0.4 y 0.5 y 0.6 z 0.4 z 0.5 z 0.6")', default=['x','0.4','x','0.5','x','0.6','y','0.4','y','0.5','y','0.6','z','0.4','z','0.5','z','0.6'], nargs='+') + parser.add_argument('--suboptions', help='Main options of slicer for creating the sub-reports (same as OPTIONS, see below). (default: OPTIONS)', nargs='+') + parser.add_argument('--suboutputs', help='Output options of slicer for creating the sub-reports (same as OUTPUTS, see below). (default: "S 4 1600")', default=['S','4','1600'], nargs='+') + + return parser diff --git a/bidscoin/plugins/dcm2niix2bids.py b/bidscoin/plugins/dcm2niix2bids.py index 55f15584..67bb3405 100644 --- a/bidscoin/plugins/dcm2niix2bids.py +++ b/bidscoin/plugins/dcm2niix2bids.py @@ -14,14 +14,8 @@ from typing import Union from pathlib import Path from nibabel.testing import data_path -try: - from bidscoin import bcoin, bids - from bidscoin.utilities import physio -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - sys.path.append(str(Path(__file__).parents[1]/'utilities')) - import bcoin, bids, physio +from bidscoin import bcoin, bids +from bidscoin.utilities import physio LOGGER = logging.getLogger(__name__) diff --git a/bidscoin/plugins/nibabel2bids.py b/bidscoin/plugins/nibabel2bids.py index 4035882c..4f0eb1fd 100644 --- a/bidscoin/plugins/nibabel2bids.py +++ b/bidscoin/plugins/nibabel2bids.py @@ -13,12 +13,7 @@ from bids_validator import BIDSValidator from typing import Union from pathlib import Path -try: - from bidscoin import bids -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bids +from bidscoin import bids LOGGER = logging.getLogger(__name__) diff --git a/bidscoin/plugins/pet2bids.py b/bidscoin/plugins/pet2bids.py index b0b3e33c..047c1c11 100644 --- a/bidscoin/plugins/pet2bids.py +++ b/bidscoin/plugins/pet2bids.py @@ -16,11 +16,7 @@ from pathlib import Path from functools import lru_cache from bids_validator import BIDSValidator - -try: - from bidscoin import bcoin, bids -except ImportError: - import bcoin, bids # This should work if bidscoin was not pip-installed +from bidscoin import bcoin, bids LOGGER = logging.getLogger(__name__) diff --git a/bidscoin/plugins/phys2bidscoin.py b/bidscoin/plugins/phys2bidscoin.py index 1211fadd..f985fb93 100644 --- a/bidscoin/plugins/phys2bidscoin.py +++ b/bidscoin/plugins/phys2bidscoin.py @@ -17,12 +17,7 @@ import ast from pathlib import Path from functools import lru_cache -try: - from bidscoin import bids -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bids +from bidscoin import bids LOGGER = logging.getLogger(__name__) diff --git a/bidscoin/plugins/spec2nii2bids.py b/bidscoin/plugins/spec2nii2bids.py index 1bed999b..3cfc31f6 100644 --- a/bidscoin/plugins/spec2nii2bids.py +++ b/bidscoin/plugins/spec2nii2bids.py @@ -11,12 +11,7 @@ import ast from bids_validator import BIDSValidator from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids +from bidscoin import bcoin, bids LOGGER = logging.getLogger(__name__) diff --git a/bidscoin/utilities/bidsparticipants.py b/bidscoin/utilities/bidsparticipants.py index b1cc0db7..a11c98bd 100755 --- a/bidscoin/utilities/bidsparticipants.py +++ b/bidscoin/utilities/bidsparticipants.py @@ -1,12 +1,5 @@ #!/usr/bin/env python3 -""" -(Re)scans data sets in the source folder for subject metadata to populate the participants.tsv -file in the bids directory, e.g. after you renamed (be careful there!), added or deleted data -in the bids folder yourself. - -Provenance information, warnings and error messages are stored in the -bidsfolder/code/bidscoin/bidsparticipants.log file. -""" +"""(Re)scans data sets in the source folder for subject metadata (See also cli/_bidsparticipants.py)""" import pandas as pd import json @@ -15,12 +8,11 @@ from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bcoin, bids, __version__ def scanpersonals(bidsmap: dict, session: Path, personals: dict) -> bool: @@ -83,7 +75,7 @@ def bidsparticipants(rawfolder: str, bidsfolder: str, keys: list, bidsmapfile: s else: bcoin.setup_logging(bidsfolder/'code'/'bidscoin'/'bidsparticipants.log') LOGGER.info('') - LOGGER.info(f"-------------- START bidsparticipants {bcoin.version()} ------------") + LOGGER.info(f"-------------- START bidsparticipants {__version__} ------------") LOGGER.info(f">>> bidsparticipants sourcefolder={rawfolder} bidsfolder={bidsfolder} bidsmap={bidsmapfile}") # Get the bidsmap sub-/ses-prefix from the bidsmap YAML-file @@ -187,22 +179,9 @@ def bidsparticipants(rawfolder: str, bidsfolder: str, keys: list, bidsmapfile: s def main(): """Console script usage""" - # Parse the input arguments and run bidsparticipants(args) - import argparse - import textwrap - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=textwrap.dedent(__doc__), - epilog='examples:\n' - ' bidsparticipants myproject/raw myproject/bids\n' - ' bidsparticipants myproject/raw myproject/bids -k participant_id age sex\n ') - parser.add_argument('sourcefolder', help='The study root folder containing the raw source data folders') - parser.add_argument('bidsfolder', help='The destination / output folder with the bids data') - parser.add_argument('-k','--keys', help="Space separated list of the participants.tsv columns. Default: 'session_id' 'age' 'sex' 'size' 'weight'", nargs='+', default=['age', 'sex', 'size', 'weight']) # NB: session_id is default - parser.add_argument('-d','--dryrun', help='Do not save anything, only print the participants info on screen', action='store_true') - parser.add_argument('-b','--bidsmap', help='The study bidsmap file with the mapping heuristics. If the bidsmap filename is relative (i.e. no "/" in the name) then it is assumed to be located in bidsfolder/code/bidscoin. Default: bidsmap.yaml', default='bidsmap.yaml') - parser.add_argument('-v','--version', help='Show the BIDS and BIDScoin version', action='version', version=f"BIDS-version:\t\t{bcoin.bidsversion()}\nBIDScoin-version:\t{bcoin.version()}") - args = parser.parse_args() + from bidscoin.cli._bidsparticipants import get_parser + args = get_parser().parse_args() bidsparticipants(rawfolder = args.sourcefolder, bidsfolder = args.bidsfolder, keys = args.keys, diff --git a/bidscoin/utilities/dicomsort.py b/bidscoin/utilities/dicomsort.py index f5f5a312..2570d549 100755 --- a/bidscoin/utilities/dicomsort.py +++ b/bidscoin/utilities/dicomsort.py @@ -1,10 +1,5 @@ #!/usr/bin/env python3 -""" -Sorts and/or renames DICOM files into local subfolders, e.g. with 3-digit SeriesNumber-SeriesDescription -folder names (i.e. following the same listing as on the scanner console) - -Supports flat DICOM as well as multi-subject/session DICOMDIR file structures. -""" +"""Sorts and/or renames DICOM files into local subfolders (See also cli/_dicomsort.py)""" import re import logging @@ -12,12 +7,11 @@ from pydicom import fileset from pathlib import Path from typing import List -try: - from bidscoin import bcoin, bids -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bcoin, bids LOGGER = logging.getLogger(__name__) @@ -215,31 +209,12 @@ def sortsessions(sourcefolder: Path, subprefix: str='', sesprefix: str='', folde def main(): """Console script usage""" - # Parse the input arguments and run the sortsessions(args) - import argparse - import textwrap - - class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass - - parser = argparse.ArgumentParser(formatter_class=CustomFormatter, - description=textwrap.dedent(__doc__), - epilog='examples:\n' - ' dicomsort sub-011/ses-mri01\n' - ' dicomsort sub-011/ses-mri01/DICOMDIR -n {AcquisitionNumber:05d}_{InstanceNumber:05d}.dcm\n' - ' dicomsort myproject/raw/DICOMDIR --subprefix pat^ --sesprefix\n ') - parser.add_argument('dicomsource', help='The root folder containing the dicomsource/[sub/][ses/] dicomfiles or the DICOMDIR file') - parser.add_argument('-i','--subprefix', help='Provide a prefix string for recursive sorting of dicomsource/subject subfolders (e.g. "sub-")') - parser.add_argument('-j','--sesprefix', help='Provide a prefix string for recursive sorting of dicomsource/subject/session subfolders (e.g. "ses-")') - parser.add_argument('-f','--folderscheme', help='Naming scheme for the sorted DICOM Series subfolders. Follows the Python string formatting syntax with DICOM field names in curly bracers with an optional number of digits for numeric fields. Sorting in subfolders is skipped when an empty folderscheme is given (but note that renaming the filenames can still be performed)', default='{SeriesNumber:03d}-{SeriesDescription}') - parser.add_argument('-n','--namescheme', help='Optional naming scheme that can be provided to rename the DICOM files. Follows the Python string formatting syntax with DICOM field names in curly bracers with an optional number of digits for numeric fields. Use e.g. "{PatientName}_{SeriesNumber:03d}_{SeriesDescription}_{AcquisitionNumber:05d}_{InstanceNumber:05d}.dcm" or "{InstanceNumber:05d}_{SOPInstanceUID}.IMA" for default names') - parser.add_argument('-p','--pattern', help='The regular expression pattern used in re.match(pattern, dicomfile) to select the dicom files', default=r'.*\.(IMA|dcm)$') - parser.add_argument('--force', help='Sort the DICOM data even the DICOM fields of the folder/name scheme are not in the data', action='store_true') - parser.add_argument('-d','--dryrun', help='Only print the dicomsort commands without actually doing anything', action='store_true') - args = parser.parse_args() + from bidscoin.cli._dicomsort import get_parser # Set-up logging bcoin.setup_logging() + args = get_parser().parse_args() sortsessions(sourcefolder = args.dicomsource, subprefix = args.subprefix, sesprefix = args.sesprefix, diff --git a/bidscoin/utilities/physio2tsv.py b/bidscoin/utilities/physio2tsv.py index bd1acb77..7fc33781 100755 --- a/bidscoin/utilities/physio2tsv.py +++ b/bidscoin/utilities/physio2tsv.py @@ -1,17 +1,14 @@ #!/usr/bin/env python3 -""" -Reads and plots active (i.e. non-zero) signals from SIEMENS advanced physiological log / DICOM files +""" Reads and writes SIEMENS advanced physiological log / DICOM files (See also cli/_physio2tsv.py)""" -This function expects to find either a combination of individual logfiles (*_ECG.log, *_RESP.log, -*_PULS.log, *_EXT.log, *_Info.log) generated by >=R013 sequences, or a single encoded "_PHYSIO" DICOM -file generated by >=R015 sequences. -""" - -import logging, coloredlogs -try: - import bidscoin.utilities.physio as ph -except ImportError: - import physio as ph # This should work if bidscoin was not pip-installed +import logging +import coloredlogs +from importlib.util import find_spec +if find_spec('bidscoin') is None: + import sys + from pathlib import Path + sys.path.append(str(Path(__file__).parents[2])) +import bidscoin.utilities.physio as ph # Set-up logging LOGGER = logging.getLogger(__name__) @@ -22,19 +19,9 @@ def main(): """Console script usage""" - # Parse the input arguments and run physio2tsv(args) - import argparse - - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=__doc__, - epilog='examples:\n' - ' physio2tsv myproject/sub-001/MR000000.dcm myproject/bids/sub-001/func/sub-001_physio\n' - ' physio2tsv myproject/sub-001/Physio_20200428_142451_007e910e-02d9-4d7a-8fdb-8e3568be8322 myproject/bids/sub-001/func/sub-001_physio\n\n' - '@author: Marcel Zwiers\n ') - parser.add_argument('physiofile', help="Either the fullpath of the DICOM file or the basename of the PHYSIO logfiles (fullpath without suffix and file extension, e.g. 'foo/bar/Physio_DATE_TIME_UUID'") - parser.add_argument('tsvfile', help="The fullpath of the BIDS filenames, e.g. 'foo/bids/sub-001/func/sub-001_physio'") - args = parser.parse_args() + from bidscoin.cli._physio2tsv import get_parser + args = get_parser().parse_args() physio = ph.readphysio(args.physiofile) ph.physio2tsv(physio, args.tsvfile) diff --git a/bidscoin/utilities/plotphysio.py b/bidscoin/utilities/plotphysio.py index be183b98..305ef494 100755 --- a/bidscoin/utilities/plotphysio.py +++ b/bidscoin/utilities/plotphysio.py @@ -1,17 +1,14 @@ #!/usr/bin/env python3 -""" -Reads and plots active (i.e. non-zero) signals from SIEMENS advanced physiological log / DICOM files +""" Plots SIEMENS advanced physiological log / DICOM files (See also cli/_plotphysio.py)""" -This function expects to find either a combination of individual logfiles (*_ECG.log, *_RESP.log, -*_PULS.log, *_EXT.log, *_Info.log) generated by >=R013 sequences, or a single encoded "_PHYSIO" DICOM -file generated by >=R015 sequences. -""" - -import logging, coloredlogs -try: - import bidscoin.utilities.physio as ph -except ImportError: - import physio as ph # This should work if bidscoin was not pip-installed +import logging +import coloredlogs +from importlib.util import find_spec +if find_spec('bidscoin') is None: + import sys + from pathlib import Path + sys.path.append(str(Path(__file__).parents[2])) +import bidscoin.utilities.physio as ph # Set-up logging LOGGER = logging.getLogger(__name__) @@ -22,19 +19,9 @@ def main(): """Console script usage""" - # Parse the input arguments and run plotphysio(args) - import argparse - - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=__doc__, - epilog='examples:\n' - ' plotphysio myproject/sub-001/MR000000.dcm\n' - ' plotphysio -s 2000 myproject/sub-001/Physio_20200428_142451_007e910e-02d9-4d7a-8fdb-8e3568be8322\n\n' - '@author: Marcel Zwiers\n ') - parser.add_argument('filename', help="Either the fullpath of the DICOM file or the basename of the PHYSIO logfiles (fullpath without suffix and file extension, e.g. 'foo/bar/Physio_DATE_TIME_UUID'") - parser.add_argument('-s','--showsamples', help='The nr of plotted samples of the physiological traces (default: 1000, nothing is plotted if 0)', default=1000, type=int) - args = parser.parse_args() + from bidscoin.cli._plotphysio import get_parser + args = get_parser().parse_args() physio = ph.readphysio(args.filename) ph.plotphysio(physio, args.showsamples) diff --git a/bidscoin/utilities/rawmapper.py b/bidscoin/utilities/rawmapper.py index eca73ca3..4a9358b8 100755 --- a/bidscoin/utilities/rawmapper.py +++ b/bidscoin/utilities/rawmapper.py @@ -1,21 +1,14 @@ #!/usr/bin/env python3 -""" -Maps out the values of a dicom attribute of all subjects in the sourcefolder, saves the result -in a mapper-file and, optionally, uses the dicom values to rename the sub-/ses-id's of the -subfolders. This latter option can be used, e.g. when an alternative subject id was entered in -the [Additional info] field during subject registration at the scanner console (i.e. this data -is stored in the dicom attribute named 'PatientComments') -""" +"""Maps out the values of a dicom attribute of all subjects in the sourcefolder (See also cli/_rawmapper.py)""" import warnings import shutil from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parents[1])) # This should work if bidscoin was not pip-installed - import bcoin, bids + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin import bcoin, bids def rawmapper(rawfolder, outfolder: str='', sessions: tuple=(), rename: bool=False, force: bool=False, dicomfield: tuple=('PatientComments',), wildcard: str='*', subprefix: str='sub-', sesprefix: str='ses-', dryrun: bool=False) -> None: @@ -143,33 +136,9 @@ def rawmapper(rawfolder, outfolder: str='', sessions: tuple=(), rename: bool=Fal def main(): """Console script usage""" - # Parse the input arguments and run the rawmapper(args) - import argparse - import textwrap - - class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass - - parser = argparse.ArgumentParser(formatter_class=CustomFormatter, - description=textwrap.dedent(__doc__), - epilog='examples:\n' - ' rawmapper myproject/raw\n' - ' rawmapper myproject/raw -f AcquisitionDate\n' - ' rawmapper myproject/raw -s sub-100/ses-mri01 sub-126/ses-mri01\n' - ' rawmapper myproject/raw -r -f ManufacturerModelName AcquisitionDate --dryrun\n' - ' rawmapper myproject/raw -r -s sub-1*/* sub-2*/ses-mri01 --dryrun\n' - ' rawmapper -f EchoTime -w *fMRI* myproject/raw\n ') - parser.add_argument('sourcefolder', help='The source folder with the raw data in sub-#/ses-#/series organisation') - parser.add_argument('-s','--sessions', help='Space separated list of selected sub-#/ses-# names / folders to be processed. Otherwise all sessions in the bidsfolder will be selected', nargs='+') - parser.add_argument('-f','--field', help='The fieldname(s) of the dicom attribute(s) used to rename or map the subid/sesid foldernames', default=['PatientComments', 'ImageComments'], nargs='+') - parser.add_argument('-w','--wildcard', help='The Unix style pathname pattern expansion that is used to select the series from which the dicomfield is being mapped (can contain wildcards)', default='*') - parser.add_argument('-o','--outfolder', help='The mapper-file is normally saved in sourcefolder or, when using this option, in outfolder') - parser.add_argument('-r','--rename', help='Rename sub-subid/ses-sesid directories in the sourcefolder to sub-dcmval/ses-dcmval', action='store_true') - parser.add_argument('-c','--clobber', help='Rename the sub/ses directories, even if the target-directory already exists', action='store_true') - parser.add_argument('-n','--subprefix', help="The prefix common for all the source subject-folders. Use a '*' wildcard if there is no prefix", default='sub-') - parser.add_argument('-m','--sesprefix', help="The prefix common for all the source session-folders. Use a '*' wildcard if there is no prefix or an empty value if there are no sessions", nargs='?', default='ses-') - parser.add_argument('-d','--dryrun', help='Dryrun (test) the mapping or renaming of the sub-subid/ses-sesid directories (i.e. nothing is stored on disk and directory names are not actually changed))', action='store_true') - args = parser.parse_args() + from bidscoin.cli._rawmapper import get_parser + args = get_parser().parse_args() rawmapper(rawfolder = args.sourcefolder, outfolder = args.outfolder, sessions = args.sessions, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0129a840..6777cf43 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -101,7 +101,7 @@ All notable changes to this project will be documented in this file. The format - A BIDScoin installation test (`bidscoin -t`) - Option to install extra packages, such as phys2bids - A bidseditor button to save the Options to a (default) template bidsmap -- Sub-/ses-prefix settings and BIDS / extra_data / excluded datatypes in bidsmap['Options']['bidscoin'] +- Sub-/ses-prefix settings and BIDS / extra_data / excluded datatypes in `bidsmap['Options']['bidscoin']` - Regular expressions for extracting property and attribute substrings from dynamic values via a <\> syntax - A plugin for spec2nii to convert MR spectroscopy data - An experimental plugin for phys2bids to convert physiological data diff --git a/docs/workflow.rst b/docs/workflow.rst index 320ccf6f..cab4dc46 100644 --- a/docs/workflow.rst +++ b/docs/workflow.rst @@ -62,7 +62,7 @@ Step 1a: Running the bidsmapper your subject folders do not have a prefix. Default: the value of the study/template bidsmap, e.g. 'sub-' -m SESPREFIX, --sesprefix SESPREFIX - The prefix common for all the source session-folders (e.g. 'M_' is the + The prefix common for all the source session-folders (e.g. `M_` is the subprefix if session folders are named 'M_pre', 'M_post', ..). Use '*' when your session folders do not have a prefix. Default: the value of the study/template bidsmap, e.g. 'ses-' diff --git a/pyproject.toml b/pyproject.toml index 5ce111c1..c224c990 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later [build-system] -requires = ['setuptools >= 61.0.0', 'wheel'] +requires = ['setuptools >= 61.0.0', 'wheel', 'argparse-manpage[setuptools]', 'tomli >= 1.1.0 ; python_version < "3.11"'] build-backend = 'setuptools.build_meta' [project] @@ -59,8 +59,24 @@ rawmapper = 'bidscoin.utilities.rawmapper:main' physio2tsv = 'bidscoin.utilities.physio2tsv:main' plotphysio = 'bidscoin.utilities.plotphysio:main' +[tool.build_manpages] +manpages = ['man/bidscoin.1:function=get_parser:pyfile=bidscoin/cli/_bcoin.py', + 'man/bidseditor.1:function=get_parser:pyfile=bidscoin/cli/_bidseditor.py', + 'man/bidsmapper.1:function=get_parser:pyfile=bidscoin/cli/_bidsmapper.py', + 'man/bidscoiner.1:function=get_parser:pyfile=bidscoin/cli/_bidscoiner.py', + 'man/echocombine.1:function=get_parser:pyfile=bidscoin/cli/_echocombine.py', + 'man/deface.1:function=get_parser:pyfile=bidscoin/cli/_deface.py', + 'man/medeface.1:function=get_parser:pyfile=bidscoin/cli/_medeface.py', + 'man/skullstrip.1:function=get_parser:pyfile=bidscoin/cli/_skullstrip.py', + 'man/slicereport.1:function=get_parser:pyfile=bidscoin/cli/_slicereport.py', + 'man/dicomsort.1:function=get_parser:pyfile=bidscoin/cli/_dicomsort.py', + 'man/bidsparticipants.1:function=get_parser:pyfile=bidscoin/cli/_bidsparticipants.py', + 'man/rawmapper.1:function=get_parser:pyfile=bidscoin/cli/_rawmapper.py', + 'man/physio2tsv.1:function=get_parser:pyfile=bidscoin/cli/_physio2tsv.py', + 'man/plotphysio.1:function=get_parser:pyfile=bidscoin/cli/_plotphysio.py'] + [tool.setuptools.package-data] -'*' = ['*VERSION', '*.yaml', 'bidscoin_logo.png', 'bidscoin.ico', 'rightarrow.png'] # package names should match these glob patterns (["*"] by default) +'*' = ['*.yaml', 'BIDS_VERSION', 'bidscoin.ico', 'bidscoin_logo.png', 'rightarrow.png'] # package names should match these glob patterns (["*"] by default) [tool.pytest.ini_options] testpaths = ['tests'] diff --git a/setup.py b/setup.py index 60684932..7df38e03 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,6 @@ from setuptools import setup +from build_manpages import build_manpages, get_build_py_cmd, get_install_cmd -setup() +setup(cmdclass = {'build_manpages': build_manpages, + 'build_py': get_build_py_cmd(), + 'install': get_install_cmd()}) diff --git a/tests/conftest.py b/tests/conftest.py index 35aa1b9d..c8df3e57 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,12 +3,11 @@ import json from pathlib import Path from pydicom.data import get_testdata_file -try: - from bidscoin.utilities import dicomsort -except ImportError: +from importlib.util import find_spec +if find_spec('bidscoin') is None: import sys - sys.path.append(str(Path(__file__).parents[1]/'bidscoin'/'utilities')) # This should work if bidscoin was not pip-installed - import dicomsort + sys.path.append(str(Path(__file__).parents[2])) +from bidscoin.utilities import dicomsort @pytest.fixture(scope='session') diff --git a/tests/test_bcoin.py b/tests/test_bcoin.py index 10686e81..b0738ab5 100644 --- a/tests/test_bcoin.py +++ b/tests/test_bcoin.py @@ -1,26 +1,21 @@ -from pathlib import Path -try: - from bidscoin import bcoin -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1]/'bidscoin')) # This should work if bidscoin was not pip-installed - import bcoin +import bidscoin +from bidscoin import bcoin bcoin.setup_logging() -assert bcoin.schemafolder.is_dir() -assert bcoin.heuristicsfolder.is_dir() -assert bcoin.pluginfolder.is_dir() -assert bcoin.bidsmap_template.is_file() +assert bidscoin.schemafolder.is_dir() +assert bidscoin.heuristicsfolder.is_dir() +assert bidscoin.pluginfolder.is_dir() +assert bidscoin.bidsmap_template.is_file() def test_version(): - assert isinstance(bcoin.version(False), str) - assert isinstance(bcoin.version(True), tuple) + assert isinstance(bidscoin.version(False), str) + assert isinstance(bidscoin.version(True), tuple) def test_bidsversion(): - assert isinstance(bcoin.bidsversion(), str) + assert isinstance(bidscoin.bidsversion(), str) def test_runcommand(): diff --git a/tests/test_bids.py b/tests/test_bids.py index a82653d5..149f0b12 100644 --- a/tests/test_bids.py +++ b/tests/test_bids.py @@ -3,17 +3,11 @@ import shutil import re import json +import ruamel.yaml.comments from pathlib import Path from nibabel.testing import data_path - -import ruamel.yaml.comments from pydicom.data import get_testdata_file -try: - from bidscoin import bcoin, bids -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1]/'bidscoin')) # This should work if bidscoin was not pip-installed - import bcoin, bids +from bidscoin import bcoin, bids bcoin.setup_logging() diff --git a/tests/test_bidscoiner.py b/tests/test_bidscoiner.py index a52a37d7..7167c0de 100644 --- a/tests/test_bidscoiner.py +++ b/tests/test_bidscoiner.py @@ -1,18 +1,12 @@ import json -from pathlib import Path -try: - from bidscoin import bcoin, bidsmapper, bidscoiner -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1]/'bidscoin')) # This should work if bidscoin was not pip-installed - import bcoin, bidsmapper, bidscoiner +from bidscoin import bcoin, bidsmapper, bidscoiner, bidsmap_template bcoin.setup_logging() def test_bidscoiner(raw_dicomdir, bids_dicomdir, bidsmap_dicomdir): if not bidsmap_dicomdir.is_file(): - bidsmapper.bidsmapper(raw_dicomdir, bids_dicomdir, bidsmap_dicomdir, bcoin.bidsmap_template, [], 'Doe^', '*', unzip='', noeditor=True, force=True) + bidsmapper.bidsmapper(raw_dicomdir, bids_dicomdir, bidsmap_dicomdir, bidsmap_template, [], 'Doe^', '*', unzip='', noeditor=True, force=True) try: (bidsmap_dicomdir.parent/'bidsmapper.errors').unlink(missing_ok=True) except Exception: diff --git a/tests/test_bidsmapper.py b/tests/test_bidsmapper.py index e96f5aae..1fee3fda 100644 --- a/tests/test_bidsmapper.py +++ b/tests/test_bidsmapper.py @@ -1,14 +1,6 @@ -from pathlib import Path import pytest import re -try: - from bidscoin import bcoin, bids, bidsmapper - from bidscoin.utilities import dicomsort -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1]/'bidscoin')) # This should work if bidscoin was not pip-installed - sys.path.append(str(Path(__file__).parents[1]/'bidscoin'/'utilities')) - import bcoin, bids, bidsmapper, dicomsort +from bidscoin import bcoin, bidsmapper bcoin.setup_logging() diff --git a/tests/test_load_bidsmap.py b/tests/test_load_bidsmap.py new file mode 100644 index 00000000..10858187 --- /dev/null +++ b/tests/test_load_bidsmap.py @@ -0,0 +1,21 @@ +import pytest +from pathlib import Path +from bidscoin import bcoin, bids + +# setup logger +bcoin.setup_logging() + + +@pytest.fixture() +def setup_bidsmaps(): + template_bidsmap_path = Path('../bidscoin/heuristics/bidsmap_dccn.yaml') + bidsmap_path = Path('tests/test_data/bidsmap.yaml') + full_bidsmap_path = Path(bidsmap_path.resolve()) + return {'template_bidsmap_path': template_bidsmap_path, 'full_bidsmap_path': full_bidsmap_path} + + +def test_template_bidsmap_is_valid(setup_bidsmaps): + template_bidsmap, _ = bids.load_bidsmap(setup_bidsmaps['template_bidsmap_path']) + is_valid = bids.check_bidsmap(template_bidsmap) + for each in is_valid: + assert each is None diff --git a/tests/test_pet2bids.py b/tests/test_pet2bids.py deleted file mode 100644 index 987d6d16..00000000 --- a/tests/test_pet2bids.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging -import pytest -from pathlib import Path -try: - from bidscoin import bcoin, bids - from bidscoin.plugins import pet2bids -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1]/'bidscoin')) # This should work if bidscoin was not pip-installed - sys.path.append(str(Path(__file__).parents[1]/'plugins')) # This should work if bidscoin was not pip-installed - import bcoin, bids - from plugins import pet2bids - -# setup logger -LOGGER = logging.getLogger(__name__) -bcoin.setup_logging() - - -@pytest.fixture() -def setup_bidsmaps(): - template_bidsmap_path = Path('../bidscoin/heuristics/bidsmap_dccn.yaml') - bidsmap_path = Path('tests/test_data/bidsmap.yaml') - full_bidsmap_path = Path(bidsmap_path.resolve()) - return {'template_bidsmap_path': template_bidsmap_path, 'full_bidsmap_path': full_bidsmap_path} - - -def test_template_bidsmap_is_valid(setup_bidsmaps): - template_bidsmap, _ = bids.load_bidsmap(setup_bidsmaps['template_bidsmap_path']) - is_valid = bids.check_bidsmap(template_bidsmap) - for each in is_valid: - assert each is None diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 0ac9b995..079ef781 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -6,13 +6,7 @@ import pytest import inspect -from pathlib import Path -try: - from bidscoin import bcoin, bids -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1]/'bidscoin')) # This should work if bidscoin was not pip-installed - import bcoin, bids +from bidscoin import bcoin, bids bcoin.setup_logging() template, _ = bids.load_bidsmap(bcoin.bidsmap_template, check=(False,False,False)) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index f9a9a1be..eeedbbe9 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -2,15 +2,8 @@ import csv from pydicom.data import get_testdata_file from pathlib import Path -try: - from bidscoin import bcoin - from bidscoin.utilities import dicomsort, rawmapper, bidsparticipants -except ImportError: - import sys - sys.path.append(str(Path(__file__).parents[1]/'bidscoin')) # This should work if bidscoin was not pip-installed - sys.path.append(str(Path(__file__).parents[1]/'bidscoin'/'utilities')) - import bcoin - import dicomsort, rawmapper, bidsparticipants +from bidscoin import bcoin +from bidscoin.utilities import dicomsort, rawmapper, bidsparticipants bcoin.setup_logging() diff --git a/tox.ini b/tox.ini index d09e149b..eba1df29 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,9 @@ envlist = py38, py39, py310, py311 [testenv] extras = all -deps = pytest +deps = + pytest + argparse-manpage[setuptools] commands = pytest [gh-actions]