nifti2bids 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nifti2bids/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """
2
+ Post-hoc BIDS conversion toolkit for NIfTI datasets without original DICOMs.
3
+ ----------------------------------------------------------------------------
4
+ Documentation can be found at https://nifti2bids.readthedocs.io.
5
+
6
+ Submodules
7
+ ----------
8
+ bids -- Operations related to initializing and creating BIDs compliant files
9
+
10
+ io -- Generic operations related to loading NIfTI data
11
+
12
+ logging -- Set up a logger using ``RichHandler`` as the default handler if a root or
13
+ module specific handler is not available
14
+
15
+ metadata -- Operations related to extracting metadata information from NIfTI images
16
+
17
+ simulate -- Simulate a basic NIfTI image for testing purposes
18
+ """
19
+
20
+ __version__ = "0.1.1"
@@ -0,0 +1,53 @@
1
+ """Decorator functions."""
2
+
3
+ import functools, inspect
4
+
5
+ from typing import Any, Callable
6
+
7
+ from ._helpers import list_to_str
8
+
9
+
10
+ def check_all_none(parameter_names: list[str]) -> Callable:
11
+ """
12
+ Checks if specific parameters are assigned ``None``.
13
+
14
+ Parameters
15
+ ----------
16
+ parameter_names: :obj:`list[str]`
17
+ List of parameter names to check.
18
+
19
+ Returns
20
+ -------
21
+ Callable
22
+ Decorator function wrapping target function.
23
+ """
24
+
25
+ def decorator(func: Callable) -> Callable:
26
+ signature = inspect.signature(func)
27
+ if invalid_params := [
28
+ param
29
+ for param in parameter_names
30
+ if param not in signature.parameters.keys()
31
+ ]:
32
+ raise NameError(
33
+ "Error in ``parameter_names`` of decorator. The following "
34
+ f"parameters are not in the signature of '{func.__name__}': "
35
+ f"{list_to_str(invalid_params)}."
36
+ )
37
+
38
+ @functools.wraps(func)
39
+ def wrapper(*args: Any, **kwargs: Any) -> Callable:
40
+ bound_args = signature.bind(*args, **kwargs)
41
+ bound_args.apply_defaults()
42
+ all_param_values = [bound_args.arguments[name] for name in parameter_names]
43
+ if all(value is None for value in all_param_values):
44
+ raise ValueError(
45
+ "All of the following arguments cannot be None, "
46
+ f"one must be specified: {list_to_str(parameter_names)}."
47
+ )
48
+
49
+ return func(*args, **kwargs)
50
+
51
+ return wrapper
52
+
53
+ return decorator
@@ -0,0 +1,51 @@
1
+ """Custom exceptions."""
2
+
3
+ from typing import Literal, Optional
4
+
5
+
6
+ class SliceAxisError(Exception):
7
+ """
8
+ Incorrect slice axis.
9
+
10
+ Raised when the number of slices does not match "slice_end" plus one.
11
+
12
+ Parameters
13
+ ----------
14
+ slice_axis: :obj:`Literal["x", "y", "z"]`
15
+ The specified slice dimension.
16
+
17
+ n_slices: :obj:`int`
18
+ The number of slices from the specified ``slice_axis``.
19
+
20
+ slice_end: :obj:`int`
21
+ The number of slices specified by "slice_end" in the NIfTI header.
22
+
23
+ message: :obj:`str` or :obj:`None`:
24
+ The error message. If None, a default error message is used.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ slice_axis: Literal["x", "y", "z"],
30
+ n_slices: int,
31
+ slice_end: int,
32
+ message: Optional[str] = None,
33
+ ):
34
+ if not message:
35
+ self.message = (
36
+ "Incorrect slice axis. Number of slices for "
37
+ f"{slice_axis} dimension is {n_slices} but "
38
+ f"'slice_end' in NIfTI header is {slice_end}."
39
+ )
40
+ else:
41
+ self.message = message
42
+
43
+ super().__init__(self.message)
44
+
45
+
46
+ class DataDimensionError(Exception):
47
+ """
48
+ Incorrect data dimensionality.
49
+ """
50
+
51
+ pass
nifti2bids/_helpers.py ADDED
@@ -0,0 +1,6 @@
1
+ """Helper functions."""
2
+
3
+
4
+ def list_to_str(str_list: list[str]) -> None:
5
+ """Converts a list containing strings to a string."""
6
+ return ", ".join(["'{a}'".format(a=x) for x in str_list])
nifti2bids/bids.py ADDED
@@ -0,0 +1,192 @@
1
+ """Module for creating BIDS compliant files."""
2
+
3
+ import os, json
4
+ from typing import Optional
5
+
6
+ import pandas as pd
7
+
8
+ from nifti2bids.io import _copy_file, glob_contents
9
+
10
+
11
+ def create_bids_file(
12
+ nifti_file: str,
13
+ subj_id: str | int,
14
+ desc: str,
15
+ ses_id: Optional[str | int] = None,
16
+ task_id: Optional[str] = None,
17
+ run_id: Optional[str | int] = None,
18
+ dst_dir: str = None,
19
+ remove_src_file: bool = False,
20
+ return_bids_filename: bool = False,
21
+ ) -> str | None:
22
+ """
23
+ Create a BIDS compliant filename with required and optional entities.
24
+
25
+ Parameters
26
+ ----------
27
+ nifti_file: :obj:`str`
28
+ Path to NIfTI image.
29
+
30
+ sub_id: :obj:`str` or :obj:`int`
31
+ Subject ID (i.e. 01, 101, etc).
32
+
33
+ desc: :obj:`str`
34
+ Description of the file (i.e., T1w, bold, etc).
35
+
36
+ ses_id: :obj:`str` or :obj:`int` or :obj:`None`, default=None
37
+ Session ID (i.e. 001, 1, etc). Optional entity.
38
+
39
+ ses_id: :obj:`str` or :obj:`int` or :obj:`None`, default=None
40
+ Session ID (i.e. 001, 1, etc). Optional entity.
41
+
42
+ task_id: :obj:`str` or :obj:`None`, default=None
43
+ Task ID (i.e. flanker, n_back, etc). Optional entity.
44
+
45
+ run_id: :obj:`str` or :obj:`int` or :obj:`None`, default=None
46
+ Run ID (i.e. 001, 1, etc). Optional entity.
47
+
48
+ dst_dir: :obj:`str`, default=None
49
+ Directory name to copy the BIDS file to. If None, then the
50
+ BIDS file is copied to the same directory as
51
+
52
+ remove_src_file: :obj:`str`, default=False
53
+ Delete the source file if True.
54
+
55
+ return_bids_filename: :obj:`str`, default=False
56
+ Returns the full BIDS filename if True.
57
+
58
+ Returns
59
+ -------
60
+ None or str
61
+ If ``return_bids_filename`` is True, then the BIDS filename is
62
+ returned.
63
+
64
+ Note
65
+ ----
66
+ There are additional entities that can be used that are
67
+ not included in this function.
68
+ """
69
+ bids_filename = f"sub-{subj_id}_ses-{ses_id}_task-{task_id}_" f"run-{run_id}_{desc}"
70
+ bids_filename = _strip_none_entities(bids_filename)
71
+
72
+ ext = f"{nifti_file.partition('.')[-1]}"
73
+ bids_filename += f"{ext}"
74
+ bids_filename = (
75
+ os.path.join(os.path.dirname(nifti_file), bids_filename)
76
+ if dst_dir is None
77
+ else os.path.join(dst_dir, bids_filename)
78
+ )
79
+
80
+ _copy_file(nifti_file, bids_filename, remove_src_file)
81
+
82
+ return bids_filename if return_bids_filename else None
83
+
84
+
85
+ def _strip_none_entities(bids_filename: str) -> str:
86
+ """
87
+ Removes entities with None in a BIDS compliant filename.
88
+
89
+ Parameters
90
+ ----------
91
+ bids_filename: :obj:`str`
92
+ The BIDS filename.
93
+
94
+ Returns
95
+ -------
96
+ str
97
+ BIDS filename with entities ending in None removed.
98
+
99
+ Example
100
+ -------
101
+ >>> from bidsrep.io import _strip_none_entities
102
+ >>> bids_filename = "sub-101_ses-None_task-flanker_bold.nii.gz"
103
+ >>> _strip_none_entities(bids_filename)
104
+ "sub-101_task-flanker_bold.nii.gz"
105
+ """
106
+ basename, _, ext = bids_filename.partition(".")
107
+ retained_entities = [
108
+ entity for entity in basename.split("_") if not entity.endswith("-None")
109
+ ]
110
+
111
+ return f"{'_'.join(retained_entities)}.{ext}"
112
+
113
+
114
+ def create_dataset_description(dataset_name: str, bids_version: str = "1.0.0") -> dict:
115
+ """
116
+ Generate a dataset description dictionary.
117
+
118
+ Creates a dictionary containing the name and BIDs version of a dataset.
119
+
120
+ .. versionadded:: 0.34.1
121
+
122
+ Parameters
123
+ ----------
124
+ dataset_name: :obj:`str`
125
+ Name of the dataset.
126
+
127
+ bids_version: :obj:`str`,
128
+ Version of the BIDS dataset.
129
+
130
+ derivative: :obj:`bool`, default=False
131
+ Determines if "GeneratedBy" key is added to dictionary.
132
+
133
+ Returns
134
+ -------
135
+ dict
136
+ The dataset description dictionary
137
+ """
138
+ return {"Name": dataset_name, "BIDSVersion": bids_version}
139
+
140
+
141
+ def save_dataset_description(dataset_description: dict[str, str], dst_dir: str) -> None:
142
+ """
143
+ Save a dataset description dictionary.
144
+
145
+ Saves the dataset description dictionary as a file named "dataset_description.json" to the
146
+ directory specified by ``output_dir``.
147
+
148
+ Parameters
149
+ ----------
150
+ dataset_description: :obj:`dict`
151
+ The dataset description dictionary.
152
+
153
+ dst_dir: :obj:`str`
154
+ Path to save the JSON file to.
155
+ """
156
+ with open(
157
+ os.path.join(dst_dir, "dataset_description.json"), "w", encoding="utf-8"
158
+ ) as f:
159
+ json.dump(dataset_description, f)
160
+
161
+
162
+ def create_participant_tsv(
163
+ bids_dir: str, save_df: bool = False, return_df: bool = True
164
+ ) -> pd.DataFrame | None:
165
+ """
166
+ Creates a basic participant dataframe for the "participants.tsv" file.
167
+
168
+ Parameters
169
+ ----------
170
+ bids_dir: :obj:`str`
171
+ The root of BIDS compliant directory.
172
+
173
+ save_df: :obj:`bool`, bool=False
174
+ Save the dataframe to the root of the BIDS compliant directory.
175
+
176
+ return_df: :obj:`str`
177
+ Whether or not to return the dataframe.
178
+
179
+ Returns
180
+ -------
181
+ pd.DataFrame or None
182
+ The dataframe if ``return_df`` is True.
183
+ """
184
+ participants = [
185
+ os.path.basename(folder) for folder in glob_contents(bids_dir, "*sub-*")
186
+ ]
187
+ df = pd.DataFrame({"participant_id": participants})
188
+
189
+ if save_df:
190
+ df.to_csv(os.path.join(bids_dir, "participants.tsv"), sep="\t", index=None)
191
+
192
+ return df if return_df else None
nifti2bids/io.py ADDED
@@ -0,0 +1,135 @@
1
+ """Module for input/output operations."""
2
+
3
+ import glob, os, shutil
4
+
5
+ import nibabel as nib
6
+
7
+
8
+ def load_nifti(
9
+ nifti_file_or_img: str | nib.nifti1.Nifti1Image,
10
+ ) -> nib.nifti1.Nifti1Image:
11
+ """
12
+ Loads a NIfTI image.
13
+
14
+ Loads NIfTI image when not a ``Nifti1Image`` object or
15
+ returns the image if already loaded in.
16
+
17
+ Parameters
18
+ ----------
19
+ nifti_file_or_img: :obj:`str` or :obj:`Nifti1Image`
20
+ Path to the NIfTI file or a NIfTI image.
21
+
22
+ Returns
23
+ -------
24
+ nib.nifti1.Nifti1Image
25
+ The loaded in NIfTI image.
26
+ """
27
+ nifti_img = (
28
+ nifti_file_or_img
29
+ if isinstance(nifti_file_or_img, nib.nifti1.Nifti1Image)
30
+ else nib.load(nifti_file_or_img)
31
+ )
32
+
33
+ return nifti_img
34
+
35
+
36
+ def compress_image(nifti_file: str, remove_src_file: bool = False) -> None:
37
+ """
38
+ Compresses a ".nii" image to a ".nii.gz" image.
39
+
40
+ Parameters
41
+ ----------
42
+ nifti_file: :obj:`str`
43
+ Path to the NIfTI image.
44
+
45
+ remove_src_file: :obj:`bool`
46
+ Deletes the original source image file.
47
+
48
+ Returns
49
+ -------
50
+ None
51
+ """
52
+ img = nib.load(nifti_file)
53
+ nib.save(img, nifti_file.replace(".nii", ".nii.gz"))
54
+
55
+ if remove_src_file:
56
+ os.remove(nifti_file)
57
+
58
+
59
+ def glob_contents(src_dir: str, pattern: str) -> list[str]:
60
+ """
61
+ Use glob to get contents with specific patterns.
62
+
63
+ Parameters
64
+ ----------
65
+ src_dir: :obj:`str`
66
+ The source directory.
67
+
68
+ ext: :obj:`str`
69
+ The extension.
70
+
71
+ Returns
72
+ -------
73
+ list[str]
74
+ List of contents with the pattern specified by ``pattern``.
75
+ """
76
+ return glob.glob(os.path.join(src_dir, f"*{pattern}"))
77
+
78
+
79
+ def get_nifti_header(nifti_file_or_img):
80
+ """
81
+ Get header from a NIfTI image.
82
+
83
+ Parameters
84
+ ----------
85
+ nifti_file_or_img: :obj:`str` or :obj:`Nifti1Image`
86
+ Path to the NIfTI file or a NIfTI image.
87
+
88
+ Returns
89
+ -------
90
+ nib.nifti1.Nifti1Image
91
+ The header from a NIfTI image.
92
+ """
93
+ return load_nifti(nifti_file_or_img).header
94
+
95
+
96
+ def get_nifti_affine(nifti_file_or_img):
97
+ """
98
+ Get the affine matrix from a NIfTI image.
99
+
100
+ Parameters
101
+ ----------
102
+ nifti_file_or_img: :obj:`str` or :obj:`Nifti1Image`
103
+ Path to the NIfTI file or a NIfTI image.
104
+
105
+ Returns
106
+ -------
107
+ nib.nifti1.Nifti1Image
108
+ The header from a NIfTI image.
109
+ """
110
+ return load_nifti(nifti_file_or_img).affine
111
+
112
+
113
+ def _copy_file(src_file: str, dst_file: str, remove_src_file: bool) -> None:
114
+ """
115
+ Copy a file and optionally remove the source file.
116
+
117
+ Parameters
118
+ ----------
119
+ src_file: :obj:`str`
120
+ The source file to be copied
121
+
122
+ dst_file: :obj:`str`
123
+ The new destination file.
124
+
125
+ remove_src_file: :obj:`bool`
126
+ Delete the source file if True.
127
+
128
+ Returns
129
+ -------
130
+ None
131
+ """
132
+ shutil.copy(src_file, dst_file)
133
+
134
+ if remove_src_file:
135
+ os.remove(src_file)
nifti2bids/logging.py ADDED
@@ -0,0 +1,86 @@
1
+ """Module for logging."""
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from rich.logging import RichHandler
7
+
8
+
9
+ def setup_logger(
10
+ logger_name: str = None, level: Optional[int] = None
11
+ ) -> logging.Logger:
12
+ """
13
+ Sets up the logger.
14
+
15
+ .. note::
16
+ Defaults to ``RichHandler`` if a module or root handler is
17
+ not detected.
18
+
19
+ Parameters
20
+ ----------
21
+ logger_name: :obj:`str`
22
+ Name of the logger to return, if None, the root logger is returned.
23
+
24
+ level: :obj:`int` or :obj:`None`
25
+ The logging level. If None, the logging level is not set
26
+
27
+ Returns
28
+ -------
29
+ Logger
30
+ A ``Logger`` object.
31
+ """
32
+ logger = logging.getLogger(logger_name)
33
+ if not _has_handler(logger):
34
+ logger = _add_default_handler(logger)
35
+
36
+ if level:
37
+ logger.setLevel(level)
38
+
39
+ return logger
40
+
41
+
42
+ def _has_handler(logger):
43
+ """
44
+ Check if a handler is present.
45
+
46
+ Checks the root logger and module logger.
47
+
48
+ logger: :obj:`Logger`
49
+ A logging object.
50
+
51
+ Returns
52
+ -------
53
+ bool
54
+ True if a handler is present and False if no handler is present
55
+ """
56
+ has_root_handler = logging.getLogger().hasHandlers()
57
+ has_module_handler = bool(logger.handlers)
58
+
59
+ return True if (has_root_handler or has_module_handler) else False
60
+
61
+
62
+ def _add_default_handler(logger: logging.Logger, format: str | None = None):
63
+ """
64
+ Add a default and format handler. Uses ``RichHandler`` as the default logger.
65
+
66
+ Parameters
67
+ ----------
68
+ logger: :obj:`Logger`
69
+ A logging object.
70
+
71
+ format: :obj:`str`
72
+ String specifying the format of the logged message.
73
+
74
+ Returns
75
+ -------
76
+ Logger
77
+ A logger object.
78
+ """
79
+
80
+ format = format if format else "%(asctime)s %(name)s [%(levelname)s] %(message)s"
81
+
82
+ handler = RichHandler()
83
+ handler.setFormatter(logging.Formatter(format))
84
+ logger.addHandler(handler)
85
+
86
+ return logger