diff --git a/data/participants.tsv b/data/participants.tsv index 2d076e5..8fd9e09 100644 --- a/data/participants.tsv +++ b/data/participants.tsv @@ -1,2 +1,2 @@ participant_id height weight age gender -sub-01 178 58 28 male \ No newline at end of file +sub-01 178 58 28 male diff --git a/petdeface/petdeface.py b/petdeface/petdeface.py index d29247a..341b70b 100755 --- a/petdeface/petdeface.py +++ b/petdeface/petdeface.py @@ -31,10 +31,12 @@ from mideface import ApplyMideface from mideface import Mideface from pet import WeightedAverage + from utils import run_validator except ModuleNotFoundError: from .mideface import ApplyMideface from .mideface import Mideface from .pet import WeightedAverage + from .utils import run_validator # collect version from pyproject.toml @@ -207,6 +209,10 @@ def deface(args: Union[dict, argparse.Namespace]) -> None: else: args = args + # first check to see if the dataset is bids valid + if not args.skip_bids_validator: + run_validator(args.bids_dir) + if not check_valid_fs_license() and not locate_freesurfer_license().exists(): raise Exception("You need a valid FreeSurfer license to proceed!") @@ -255,6 +261,8 @@ def deface(args: Union[dict, argparse.Namespace]) -> None: petdeface_wf = Workflow(name="petdeface_wf", base_dir=output_dir) + missing_file_errors = [] + for subject_id in subjects: try: single_subject_wf = init_single_subject_wf( @@ -265,12 +273,20 @@ def deface(args: Union[dict, argparse.Namespace]) -> None: session_label=args.session_label, session_label_exclude=args.session_label_exclude, ) - except FileNotFoundError: + except FileNotFoundError as error: single_subject_wf = None - + missing_file_errors.append(str(error)) if single_subject_wf: petdeface_wf.add_nodes([single_subject_wf]) + if ( + missing_file_errors + ): # todo add conditional later for cases where a template t1w is used + raise FileNotFoundError( + "The following subjects are missing t1w images:\n" + + "\n".join(missing_file_errors) + ) + try: petdeface_wf.write_graph("petdeface.dot", graph2use="colored", simple_form=True) except OSError as Err: @@ -748,7 +764,7 @@ def __init__( anat_only=False, subject="", n_procs=2, - skip_bids_validator=True, + skip_bids_validator=False, remove_existing=True, placement="adjacent", preview_pics=True, diff --git a/petdeface/utils.py b/petdeface/utils.py new file mode 100644 index 0000000..f176e83 --- /dev/null +++ b/petdeface/utils.py @@ -0,0 +1,50 @@ +import subprocess +from pathlib import Path +import json +import sys + + +class InvalidBIDSDataset(Exception): + def __init__(self, message, errors): + super().__init__(message) + self.errors = errors + print(f"{message}\n{errors}") + + +def deno_validator_installed(): + get_help = subprocess.run( + "bids-validator-deno --help", shell=True, capture_output=True + ) + if get_help.returncode == 0: + return True + else: + return False + + +def run_validator(bids_path): + bids_path = Path(bids_path) + if bids_path.exists(): + pass + else: + raise FileNotFoundError(bids_path) + if deno_validator_installed(): + command = f"bids-validator-deno {bids_path.resolve()} --ignoreWarnings --json --no-color" + run_validator = subprocess.run(command, shell=True, capture_output=True) + json_output = json.loads(run_validator.stdout.decode("utf-8")) + # since we've ignored warnings any issue in issue is an error + issues = json_output.get("issues").get("issues") + formatted_errors = "" + for issue in issues: + formatted_errors += "\n" + json.dumps(issue, indent=4) + if formatted_errors != "": + raise InvalidBIDSDataset( + message=f"Dataset at {bids_path} is invalid, see:", + errors=formatted_errors, + ) + + else: + raise Exception( + f"bids-validator-deno not found" + + "\nskip validation with --skip_bids_validator" + + "\nor install with pip install bids-validator-deno" + ) diff --git a/poetry.lock b/poetry.lock index a92396a..d5251b5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -83,6 +83,21 @@ files = [ [package.dependencies] bidsschematools = ">=0.10" +[[package]] +name = "bids-validator-deno" +version = "2.0.5" +description = "Typescript implementation of the BIDS validator" +optional = false +python-versions = "*" +files = [ + {file = "bids_validator_deno-2.0.5-py2.py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e7b0cd652a92c730ae00ad21e04ee430bc9cddad0bcdf5695a5fa47f17293568"}, + {file = "bids_validator_deno-2.0.5-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bc4c8f7d957900df30d7e10f7e07d790925a1a522fcf673b883bafd82e32981"}, + {file = "bids_validator_deno-2.0.5-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b676a27303d4d42b886d448964c1ec2502b1692ba440d51043b20e43778dc13"}, + {file = "bids_validator_deno-2.0.5-py2.py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:db02e9bd2e7d58967b19a06a87961d4b0a1a41f450289c730f95eb7d184fe790"}, + {file = "bids_validator_deno-2.0.5-py2.py3-none-win_amd64.whl", hash = "sha256:9462c07aeeeaf9eb07777c14689f1c6cbf2ee7075c825ffde801e3c6d3a5b7e3"}, + {file = "bids_validator_deno-2.0.5.tar.gz", hash = "sha256:7316a373c0b326f1b9ec541aaeded5f6dc6e77631940ea08c2ce6fdc590e59ae"}, +] + [[package]] name = "bidsschematools" version = "0.11.3.post3" @@ -1449,6 +1464,24 @@ test = ["coverage (>=7.2)", "pytest", "pytest-cov", "pytest-doctestplus", "pytes typing = ["tox"] zstd = ["pyzstd (>=0.14.3)"] +[[package]] +name = "niftifixer" +version = "0.0.0" +description = "Fixes Nifti's" +optional = false +python-versions = ">=3.10, <4.0" +files = [] +develop = false + +[package.dependencies] +nibabel = "^5.2.1" + +[package.source] +type = "git" +url = "https://github.com/openneuropet/nifti_fixer.git" +reference = "HEAD" +resolved_reference = "a0469bb1ad2ff3d63c12ea536657d4fd9ab76b2c" + [[package]] name = "nilearn" version = "0.10.4" @@ -3228,4 +3261,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10, <4.0" -content-hash = "d13eaeabc8d858d5540db6627dec495e106ff940245e5c50b9b2997e6b163b93" +content-hash = "6de8a7cc6be569cb4483d6784cb54ac7a6ca8cc97ccbb1dee8ee18f41895bd43" diff --git a/pyproject.toml b/pyproject.toml index f473123..3dfdec5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "petdeface" -version = "0.2.2" +version = "0.2.3" description = "A nipype PET and MR defacing pipeline for BIDS datasets utilizing FreeSurfer's MiDeFace." authors = ["Martin Nørgaard ", "Anthony Galassi <28850131+bendhouseart@users.noreply.github.com>", "Murat Bilgel "] license = "MIT" @@ -19,6 +19,9 @@ python = ">=3.10, <4.0" setuptools = "^68.1.2" petutils = "^0.0.1" niworkflows = "^1.11.0" +niftifixer = {git = "https://github.com/openneuropet/nifti_fixer.git"} +bids-validator-deno = "^2.0.5" + [tool.poetry.group.dev.dependencies] black = "^23.7.0" diff --git a/tests/test_dir_layouts.py b/tests/test_dir_layouts.py index 167253f..60db604 100644 --- a/tests/test_dir_layouts.py +++ b/tests/test_dir_layouts.py @@ -3,6 +3,7 @@ import shutil import bids from petdeface.petdeface import PetDeface +from petdeface.utils import InvalidBIDSDataset from os import cpu_count from bids.layout import BIDSLayout import subprocess @@ -34,6 +35,7 @@ def test_anat_in_first_session_folder(): / "derivatives" / "petdeface", n_procs=nthreads, + preview_pics=False, ) petdeface.run() @@ -80,6 +82,7 @@ def test_anat_in_each_session_folder(): / "derivatives" / "petdeface", n_procs=nthreads, + preview_pics=False, ) petdeface.run() @@ -117,5 +120,54 @@ def test_anat_in_subject_folder(): / "derivatives" / "petdeface", n_procs=nthreads, + preview_pics=False, ) petdeface.run() + +def test_no_anat(): + # create a temporary directory to copy the existing dataset into + with tempfile.TemporaryDirectory() as tmpdir: + shutil.copytree(data_dir, Path(tmpdir) / "no_anat") + + subject_folder = Path(tmpdir) / "no_anat" / "sub-01" + # next we delete the anat fold in the subject folder + shutil.rmtree(subject_folder / "ses-baseline" / "anat") + + # run petdeface on the copied dataset + petdeface = PetDeface( + Path(tmpdir) / "no_anat", + output_dir=Path(tmpdir) + / "no_anat_defaced" + / "derivatives" + / "petdeface", + n_procs=nthreads, + ) + + # now we want to assert that this pipeline crashes and print the error + with pytest.raises(FileNotFoundError) as exc_info: + petdeface.run() + +def test_invalid_bids(): + with tempfile.TemporaryDirectory() as tmpdir: + shutil.copytree(data_dir, Path(tmpdir) / "invalid") + # rename the files in the pet folder to a different subject id + subject_folder = Path(tmpdir) / "invalid" / "sub-01" + pet_folder = subject_folder / "ses-baseline" / "pet" + for file in pet_folder.glob("sub-01_*"): + shutil.move( + file, + pet_folder / file.name.replace("sub-01", "sub-01-bestsubject") + ) + + # run petdeface on the invalid dataset + petdeface = PetDeface( + Path(tmpdir) / "invalid", + output_dir=Path(tmpdir) / "invalid_defaced" / "derivatives" / "petdeface", + n_procs=nthreads, + ) + + # Run it and see what error gets raised + with pytest.raises(InvalidBIDSDataset) as exc_info: + petdeface.run() + assert "Dataset at" in str(exc_info.value) + \ No newline at end of file