oceanproc 0.1.0__tar.gz
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.
- oceanproc-0.1.0/PKG-INFO +29 -0
- oceanproc-0.1.0/README.md +2 -0
- oceanproc-0.1.0/oceanproc/__init__.py +1 -0
- oceanproc-0.1.0/oceanproc/bids_wrapper.py +331 -0
- oceanproc-0.1.0/oceanproc/events_long.py +153 -0
- oceanproc-0.1.0/oceanproc/firstlevel/__init__.py +0 -0
- oceanproc-0.1.0/oceanproc/firstlevel/run_glm.py +923 -0
- oceanproc-0.1.0/oceanproc/firstlevel/tests/__init__.py +0 -0
- oceanproc-0.1.0/oceanproc/firstlevel/tests/test_run_glm.py +165 -0
- oceanproc-0.1.0/oceanproc/fmriprep_wrapper.py +222 -0
- oceanproc-0.1.0/oceanproc/group_series.py +228 -0
- oceanproc-0.1.0/oceanproc/grouplevel/__init__.py +0 -0
- oceanproc-0.1.0/oceanproc/grouplevel/computations.py +80 -0
- oceanproc-0.1.0/oceanproc/grouplevel/design_mat_utils.py +54 -0
- oceanproc-0.1.0/oceanproc/grouplevel/dscalar_utils.py +51 -0
- oceanproc-0.1.0/oceanproc/grouplevel/run.py +18 -0
- oceanproc-0.1.0/oceanproc/grouplevel/utils.py +4 -0
- oceanproc-0.1.0/oceanproc/ocean_proc.py +215 -0
- oceanproc-0.1.0/oceanproc/oceanparse.py +107 -0
- oceanproc-0.1.0/oceanproc/utils.py +224 -0
- oceanproc-0.1.0/oceanproc/web/TODO.md +4 -0
- oceanproc-0.1.0/oceanproc/web/__init__.py +0 -0
- oceanproc-0.1.0/oceanproc/web/app.py +78 -0
- oceanproc-0.1.0/oceanproc/web/config.json +7 -0
- oceanproc-0.1.0/oceanproc/web/get_parser_json.py +58 -0
- oceanproc-0.1.0/oceanproc/web/static/components/ArgparseForm.js +9 -0
- oceanproc-0.1.0/oceanproc/web/static/components/ArgparseFormEntry.js +52 -0
- oceanproc-0.1.0/oceanproc/web/static/components/DirIcon.js +11 -0
- oceanproc-0.1.0/oceanproc/web/static/components/FileBrowser.js +85 -0
- oceanproc-0.1.0/oceanproc/web/static/components/FileBrowserOpenBtn.js +12 -0
- oceanproc-0.1.0/oceanproc/web/static/components/FileEntry.js +32 -0
- oceanproc-0.1.0/oceanproc/web/static/components/FileIcon.js +11 -0
- oceanproc-0.1.0/oceanproc/web/static/components/OceanprocForm.js +53 -0
- oceanproc-0.1.0/oceanproc/web/static/components/styles.css +44 -0
- oceanproc-0.1.0/oceanproc/web/templates/filesystem.html +13 -0
- oceanproc-0.1.0/oceanproc/web/templates/filesystem_fail.html +2 -0
- oceanproc-0.1.0/oceanproc/web/templates/oceanproc_form.html +23 -0
- oceanproc-0.1.0/oceanproc/web/templates/react-test.html +0 -0
- oceanproc-0.1.0/oceanproc/web/templates/username_card_component_test.html +39 -0
- oceanproc-0.1.0/pyproject.toml +36 -0
oceanproc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: oceanproc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MRI preprocessing for WUSTL Ocean labs
|
|
5
|
+
Author: Ramone Agard
|
|
6
|
+
Author-email: agardr@wustl.edu
|
|
7
|
+
Requires-Python: >=3.10,<3.13
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: asttokens (>=2.4.1,<3.0.0)
|
|
13
|
+
Requires-Dist: beautifulsoup4 (>=4.12.0)
|
|
14
|
+
Requires-Dist: flask (>=3.0.3,<4.0.0)
|
|
15
|
+
Requires-Dist: matplotlib (>=3.9.1,<4.0.0)
|
|
16
|
+
Requires-Dist: nilearn (>=0.10.4,<0.11.0)
|
|
17
|
+
Requires-Dist: numba (>=0.60.0,<0.61.0)
|
|
18
|
+
Requires-Dist: numpy (>=1.26.0)
|
|
19
|
+
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
20
|
+
Requires-Dist: scipy (>=1.13.0)
|
|
21
|
+
Requires-Dist: sphinx-rtd-theme (>=2.0.0,<3.0.0)
|
|
22
|
+
Requires-Dist: torch (>=2.5.1,<3.0.0)
|
|
23
|
+
Requires-Dist: torchaudio (>=2.5.1,<3.0.0)
|
|
24
|
+
Requires-Dist: torchvision (>=0.20.1,<0.21.0)
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Ocean-Proc
|
|
28
|
+
MRI preprocessing for WUSTL Ocean labs
|
|
29
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ["bids_wrapper", "events_long", "group_series", "ocean_proc", "oceanparse"]
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from argparse import ArgumentParser
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from glob import glob
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import shlex
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from textwrap import dedent
|
|
14
|
+
import xml.etree.ElementTree as et
|
|
15
|
+
from .utils import exit_program_early, prompt_user_continue, prepare_subprocess_logging, debug_logging, log_linebreak, flags
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
@debug_logging
|
|
21
|
+
def remove_unusable_runs(xml_file:Path, bids_data_path:Path, subject:str):
|
|
22
|
+
"""
|
|
23
|
+
Will remove unusable scans from list of scans after dcm2bids has run.
|
|
24
|
+
|
|
25
|
+
:param xml_file: Path to XML file containing quality information generated by XNAT.
|
|
26
|
+
:type xml_file: pathlib.Path
|
|
27
|
+
:param bids_data_path: Path to directory containing BIDS-compliant data for the given subject.
|
|
28
|
+
:type bids_data_path: pathlib.Path
|
|
29
|
+
:param subject: Subject ID used in BIDS-compliant data (for example, if 'sub-5000', subject is '5000').
|
|
30
|
+
:type subject: str
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
log_linebreak()
|
|
34
|
+
logger.info("####### Removing the scans marked 'unusable' #######\n")
|
|
35
|
+
|
|
36
|
+
if not xml_file.exists():
|
|
37
|
+
exit_program_early(f"Path {str(xml_file)} does not exist.")
|
|
38
|
+
|
|
39
|
+
tree = et.parse(xml_file)
|
|
40
|
+
prefix = "{" + str(tree.getroot()).split("{")[-1].split("}")[0] + "}"
|
|
41
|
+
scan_element_list = list(tree.iter(f"{prefix}scans"))
|
|
42
|
+
|
|
43
|
+
if len(scan_element_list) != 1:
|
|
44
|
+
exit_program_early(f"Error parsing the xml file provided. Found none or more than one scan groups")
|
|
45
|
+
|
|
46
|
+
scans = scan_element_list[0]
|
|
47
|
+
quality_pairs = {int(s.get("ID")) : s.find(f"{prefix}quality").text
|
|
48
|
+
for s in scans}
|
|
49
|
+
|
|
50
|
+
if len(quality_pairs) == 0:
|
|
51
|
+
exit_program_early("Could not find scan quality information in the given xml file.")
|
|
52
|
+
|
|
53
|
+
logger.debug(f"scan quality information: {quality_pairs}")
|
|
54
|
+
|
|
55
|
+
json_paths = sorted(list(p for p in (bids_data_path / f"sub-{subject}/").rglob("*.json"))) # if re.search(json_re, p.as_posix()) != None)
|
|
56
|
+
nii_paths = sorted(list(p for p in (bids_data_path / f"sub-{subject}/").rglob("*.nii.gz"))) # if re.search(json_re, p.as_posix()) != None)
|
|
57
|
+
|
|
58
|
+
if len(json_paths) == 0 or len(nii_paths) == 0:
|
|
59
|
+
exit_program_early("Could not find Nifti or JSON sidecar files in the bids directory.")
|
|
60
|
+
|
|
61
|
+
if len(json_paths) != len(nii_paths):
|
|
62
|
+
exit_program_early("Unequal amount of NIFTI and JSON files found")
|
|
63
|
+
|
|
64
|
+
for p_json, p_nii in zip(json_paths, nii_paths):
|
|
65
|
+
j = json.load(p_json.open())
|
|
66
|
+
if quality_pairs[j["SeriesNumber"]] == "unusable":
|
|
67
|
+
logger.info(f" Removing series {j['SeriesNumber']}: NIFTI:{p_nii}, JSON:{p_json}")
|
|
68
|
+
os.remove(p_json)
|
|
69
|
+
os.remove(p_nii)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@debug_logging
|
|
73
|
+
def run_dcm2bids(subject:str,
|
|
74
|
+
session:str,
|
|
75
|
+
source_dir:Path,
|
|
76
|
+
bids_output_dir:Path,
|
|
77
|
+
config_file:Path,
|
|
78
|
+
nordic_config:Path=None,
|
|
79
|
+
nifti:bool=False):
|
|
80
|
+
"""
|
|
81
|
+
Run dcm2bids with a given set of parameters.
|
|
82
|
+
|
|
83
|
+
:param subject: Subject name (ex. 'sub-5000', subject would be '5000')
|
|
84
|
+
:type subject: str
|
|
85
|
+
:param session: Session name (ex. 'ses-01', session would be '01')
|
|
86
|
+
:type session: str
|
|
87
|
+
:param source_dir: Path to 'sourcedata' directory (or wherever DICOM data is kept).
|
|
88
|
+
:type source_dir: pathlib.Path
|
|
89
|
+
:param bids_output_dir: Path to the bids directory to store the newly made NIFTI files
|
|
90
|
+
:type bids_output_dir: pathlib.Path
|
|
91
|
+
:param config_file: Path to dcm2bids config file, which maps raw sourcedata to BIDS-compliant counterpart
|
|
92
|
+
:type config_file: pathlib.Path
|
|
93
|
+
:param nordic_config: Path to second dcm2bids config file, needed for additional post processing that one BIDS config file can't handle.
|
|
94
|
+
:type nordic_config: pathlib.Path
|
|
95
|
+
:param nifti: Specify that the soure directory contains NIFTI files instead of DICOM
|
|
96
|
+
:type nifti: bool
|
|
97
|
+
:raise RuntimeError: If dcm2bids exits with a non-zero exit code.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
for p in [source_dir, bids_output_dir, config_file]:
|
|
101
|
+
if not p.exists():
|
|
102
|
+
exit_program_early(f"Path {str(p)} does not exist.")
|
|
103
|
+
|
|
104
|
+
if shutil.which('dcm2bids') == None:
|
|
105
|
+
exit_program_early("Cannot locate program 'dcm2bids', make sure it is in your PATH.")
|
|
106
|
+
|
|
107
|
+
tmp_path = bids_output_dir / f"tmp_dcm2bids/sub-{subject}_ses-{session}"
|
|
108
|
+
|
|
109
|
+
def clean_up(quiet=False):
|
|
110
|
+
try:
|
|
111
|
+
logger.debug(f"removing the temporary directory used by dcm2bids: {tmp_path}")
|
|
112
|
+
shutil.rmtree(tmp_path)
|
|
113
|
+
except Exception:
|
|
114
|
+
if not quiet:
|
|
115
|
+
logger.warning(f"There was a problem deleting the temporary directory at {tmp_path}")
|
|
116
|
+
|
|
117
|
+
if (path_that_exists := bids_output_dir/f"sub-{subject}/ses-{session}").exists():
|
|
118
|
+
ans = prompt_user_continue(dedent(f"""
|
|
119
|
+
A raw data bids path for this subject and session already exists.
|
|
120
|
+
Would you like to delete its contents and rerun dcm2bids? If not,
|
|
121
|
+
dcm2bids will be skipped.
|
|
122
|
+
"""))
|
|
123
|
+
if ans:
|
|
124
|
+
logger.debug("removing the old BIDS raw data directory and its contents")
|
|
125
|
+
shutil.rmtree(path_that_exists)
|
|
126
|
+
else:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
nifti_path = None
|
|
130
|
+
if not nifti:
|
|
131
|
+
clean_up(quiet=True)
|
|
132
|
+
run_dcm2niix(source_dir=source_dir,
|
|
133
|
+
tmp_nifti_dir=tmp_path)
|
|
134
|
+
nifti_path = tmp_path
|
|
135
|
+
else:
|
|
136
|
+
nifti_path = source_dir
|
|
137
|
+
helper_command = shlex.split(f"""{shutil.which('dcm2bids')}
|
|
138
|
+
--bids_validate
|
|
139
|
+
--skip_dcm2niix
|
|
140
|
+
-d {str(nifti_path)}
|
|
141
|
+
-p {subject}
|
|
142
|
+
-s {session}
|
|
143
|
+
-c {str(config_file)}
|
|
144
|
+
-o {str(bids_output_dir)}
|
|
145
|
+
""")
|
|
146
|
+
try:
|
|
147
|
+
log_linebreak()
|
|
148
|
+
logger.info("####### Running first round of Dcm2Bids ########\n")
|
|
149
|
+
prepare_subprocess_logging(logger)
|
|
150
|
+
with subprocess.Popen(helper_command, stdout=subprocess.PIPE) as p:
|
|
151
|
+
while p.poll() == None:
|
|
152
|
+
for line in p.stdout:
|
|
153
|
+
logger.info(line.decode("utf-8", "ignore"))
|
|
154
|
+
prepare_subprocess_logging(logger, stop=True)
|
|
155
|
+
p.kill()
|
|
156
|
+
if p.poll() != 0:
|
|
157
|
+
raise RuntimeError("'dcm2bids' has ended with a non-zero exit code.")
|
|
158
|
+
|
|
159
|
+
if nordic_config:
|
|
160
|
+
if not nordic_config.exists():
|
|
161
|
+
exit_program_early(f"Path {nordic_config} does not exist.")
|
|
162
|
+
|
|
163
|
+
nordic_run_command = shlex.split(f"""{shutil.which('dcm2bids')}
|
|
164
|
+
--bids_validate
|
|
165
|
+
--skip_dcm2niix
|
|
166
|
+
-d {str(nifti_path)}
|
|
167
|
+
-p {subject}
|
|
168
|
+
-s {session}
|
|
169
|
+
-c {str(nordic_config)}
|
|
170
|
+
-o {str(bids_output_dir)}
|
|
171
|
+
""")
|
|
172
|
+
log_linebreak()
|
|
173
|
+
logger.info("####### Running second round of Dcm2Bids ########\n")
|
|
174
|
+
prepare_subprocess_logging(logger)
|
|
175
|
+
with subprocess.Popen(nordic_run_command, stdout=subprocess.PIPE) as p:
|
|
176
|
+
while p.poll() == None:
|
|
177
|
+
for line in p.stdout:
|
|
178
|
+
logger.info(line.decode("utf-8", "ignore"))
|
|
179
|
+
prepare_subprocess_logging(logger, stop=True)
|
|
180
|
+
p.kill()
|
|
181
|
+
if p.poll() != 0:
|
|
182
|
+
raise RuntimeError("'dcm2bids' has ended with a non-zero exit code.")
|
|
183
|
+
|
|
184
|
+
# Clean up NORDIC files
|
|
185
|
+
if not flags.debug:
|
|
186
|
+
separate_nordic_files = glob(f"{str(bids_output_dir)}/sub-{subject}/ses-{session}/func/*_part-*")
|
|
187
|
+
logger.debug(f"removing the old nordic files that are not needed after mag-phase combination :\n {separate_nordic_files}")
|
|
188
|
+
for f in separate_nordic_files:
|
|
189
|
+
os.remove(f)
|
|
190
|
+
|
|
191
|
+
except RuntimeError or subprocess.CalledProcessError as e:
|
|
192
|
+
prepare_subprocess_logging(logger, stop=True)
|
|
193
|
+
logger.exception(e, stack_info=True)
|
|
194
|
+
exit_program_early("Problem running 'dcm2bids'.", None if flags.debug else clean_up)
|
|
195
|
+
if not flags.debug:
|
|
196
|
+
clean_up()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@debug_logging
|
|
200
|
+
def run_dcm2niix(source_dir:Path,
|
|
201
|
+
tmp_nifti_dir:Path,
|
|
202
|
+
clean_up_func=None):
|
|
203
|
+
"""
|
|
204
|
+
Run dcm2niix with the given input and output directories.
|
|
205
|
+
|
|
206
|
+
:param source_dir: Path to 'sourcedata' directory (or wherever DICOM data is kept).
|
|
207
|
+
:type source_dir: pathlib.Path
|
|
208
|
+
:param tmp_nifti_dir: Path to the directory to store the newly made NIFTI files
|
|
209
|
+
:type tmp_nifti_dir: pathlib.Path
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
if not source_dir.exists():
|
|
213
|
+
exit_program_early(f"Path {source_dir} does not exist.")
|
|
214
|
+
elif shutil.which('dcm2niix') == None:
|
|
215
|
+
exit_program_early("Cannot locate program 'dcm2niix', make sure it is in your PATH.")
|
|
216
|
+
|
|
217
|
+
if not tmp_nifti_dir.exists():
|
|
218
|
+
tmp_nifti_dir.mkdir(parents=True)
|
|
219
|
+
|
|
220
|
+
helper_command = shlex.split(f"""{shutil.which('dcm2niix')}
|
|
221
|
+
-b y
|
|
222
|
+
-ba y
|
|
223
|
+
-z y
|
|
224
|
+
-f %3s_%f_%p_%t
|
|
225
|
+
-o {str(tmp_nifti_dir)}
|
|
226
|
+
{str(source_dir)}
|
|
227
|
+
""")
|
|
228
|
+
try:
|
|
229
|
+
log_linebreak()
|
|
230
|
+
logger.info("####### Converting DICOM files into NIFTI #######\n")
|
|
231
|
+
prepare_subprocess_logging(logger)
|
|
232
|
+
with subprocess.Popen(helper_command, stdout=subprocess.PIPE) as p:
|
|
233
|
+
while p.poll() == None:
|
|
234
|
+
for line in p.stdout:
|
|
235
|
+
logger.info(line.decode("utf-8", "ignore"))
|
|
236
|
+
prepare_subprocess_logging(logger, stop=True)
|
|
237
|
+
p.kill()
|
|
238
|
+
if p.poll() != 0:
|
|
239
|
+
raise RuntimeError("'dcm2bniix' has ended with a non-zero exit code.")
|
|
240
|
+
except RuntimeError or subprocess.CalledProcessError as e:
|
|
241
|
+
prepare_subprocess_logging(logger, stop=True)
|
|
242
|
+
logger.exception(e, stack_info=True)
|
|
243
|
+
exit_program_early("Problem running 'dcm2niix'.",
|
|
244
|
+
exit_func=clean_up_func if clean_up_func else None)
|
|
245
|
+
|
|
246
|
+
# Delete or move extra files from short runs
|
|
247
|
+
files_to_remove = list(tmp_nifti_dir.glob("*a.nii.gz")) + list(tmp_nifti_dir.glob("*a.json"))
|
|
248
|
+
if flags.debug:
|
|
249
|
+
unused_files_dir = tmp_nifti_dir.parent / f"{tmp_nifti_dir.name}_unused"
|
|
250
|
+
unused_files_dir.mkdir(exist_ok=True)
|
|
251
|
+
logger.debug(f"moving some unused files and files created from shortened runs to directory {unused_files_dir}")
|
|
252
|
+
for f in files_to_remove:
|
|
253
|
+
shutil.move(f.resolve(), (unused_files_dir/f.name).resolve())
|
|
254
|
+
else:
|
|
255
|
+
logger.info(f"removing some unused files and files created from shortened runs :\n {[str(f) for f in files_to_remove]}")
|
|
256
|
+
for f in files_to_remove:
|
|
257
|
+
os.remove(f)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@debug_logging
|
|
261
|
+
def dicom_to_bids(subject:str,
|
|
262
|
+
session:str,
|
|
263
|
+
source_dir:Path,
|
|
264
|
+
bids_dir:Path,
|
|
265
|
+
xml_path:Path,
|
|
266
|
+
bids_config:Path,
|
|
267
|
+
nordic_config:Path=None,
|
|
268
|
+
nifti=False):
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
Facilitates the conversion of DICOM data into NIFTI data in BIDS format, and the removal of data marked 'unusable'.
|
|
272
|
+
|
|
273
|
+
:param subject: Subject name (ex. 'sub-5000', subject would be '5000')
|
|
274
|
+
:type subject: str
|
|
275
|
+
:param session: Session name (ex. 'ses-01', session would be '01')
|
|
276
|
+
:type session: str
|
|
277
|
+
:param source_dir: Path to 'sourcedata' directory (or wherever DICOM data is kept).
|
|
278
|
+
:type source_dir: pathlib.Path
|
|
279
|
+
:param bids_dir: Path to the bids directory to store the newly made NIFTI files
|
|
280
|
+
:type bids_dir: pathlib.Path
|
|
281
|
+
:param bids_config: Path to dcm2bids config file, which maps raw sourcedata to BIDS-compliant counterpart
|
|
282
|
+
:type bids_config: pathlib.Path
|
|
283
|
+
:param nordic_config: Path to second dcm2bids config file, needed for additional post processing if NORDIC data that one BIDS config file can't handle.
|
|
284
|
+
:type nordic_config: pathlib.Path
|
|
285
|
+
:param nifti: Specify that the soure directory contains NIFTI files instead of DICOM
|
|
286
|
+
:type nifti: bool
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
run_dcm2bids(subject=subject,
|
|
290
|
+
session=session,
|
|
291
|
+
source_dir=source_dir,
|
|
292
|
+
bids_output_dir=bids_dir,
|
|
293
|
+
config_file=bids_config,
|
|
294
|
+
nordic_config=nordic_config,
|
|
295
|
+
nifti=nifti)
|
|
296
|
+
|
|
297
|
+
remove_unusable_runs(xml_file=xml_path,
|
|
298
|
+
bids_data_path=bids_dir,
|
|
299
|
+
subject=subject)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
parser = ArgumentParser(prog="bids_wrapper.py",
|
|
304
|
+
description="wrapper script for dcm2bids",
|
|
305
|
+
epilog="WIP")
|
|
306
|
+
parser.add_argument("-su", "--subject", required=True,
|
|
307
|
+
help="Subject ID")
|
|
308
|
+
parser.add_argument("-se","--session", required=True,
|
|
309
|
+
help="Session ID")
|
|
310
|
+
parser.add_argument("-sd", "--source_data", type=Path, required=True,
|
|
311
|
+
help="Path to directory containing this session's DICOM files")
|
|
312
|
+
parser.add_argument("-b", "--bids_path", type=Path, required=True,
|
|
313
|
+
help="Path to the bids directory to store the newly made NIFTI files")
|
|
314
|
+
parser.add_argument("-x", "--xml_path", type=Path, required=True,
|
|
315
|
+
help="Path to this session's XML file")
|
|
316
|
+
parser.add_argument("-c", "--bids_config", type=Path, required=True,
|
|
317
|
+
help="dcm2bids config json file")
|
|
318
|
+
parser.add_argument("-n", "--nordic_config", type=Path,
|
|
319
|
+
help="Second dcm2bids config json file used for NORDIC processing")
|
|
320
|
+
parser.add_argument("--nifti", action='store_true',
|
|
321
|
+
help="Flag to specify that the source directory contains files of type NIFTI (.nii/.jsons) instead of DICOM")
|
|
322
|
+
args = parser.parse_args()
|
|
323
|
+
|
|
324
|
+
dicom_to_bids(subject=args.subject,
|
|
325
|
+
session=args.session,
|
|
326
|
+
source_dir=args.source_data,
|
|
327
|
+
bids_dir=args.bids_path,
|
|
328
|
+
xml_path=args.xml_path,
|
|
329
|
+
bids_config=args.bids_config,
|
|
330
|
+
nordic_config=args.nordic_config,
|
|
331
|
+
nifti=args.nifti)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import nibabel as nib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from .utils import exit_program_early, debug_logging, log_linebreak
|
|
7
|
+
from glob import glob
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
def find_nearest(array, value):
|
|
14
|
+
"""
|
|
15
|
+
Finds the smallest difference in 'value' and one of the
|
|
16
|
+
elements of 'array', and returns the index of the element
|
|
17
|
+
|
|
18
|
+
:param array: a list of elements to compare value to
|
|
19
|
+
:type array: a list or list-like object
|
|
20
|
+
:param value: a value to compare to elements of array
|
|
21
|
+
:type value: integer or float
|
|
22
|
+
:return: integer index of array
|
|
23
|
+
:rtype: int
|
|
24
|
+
"""
|
|
25
|
+
array = np.asarray(array)
|
|
26
|
+
idx = (np.abs(array - value)).argmin()
|
|
27
|
+
return(array[idx])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@debug_logging
|
|
31
|
+
def make_events_long(bold_run:Path, event_file:Path, output_file:Path, tr:float):
|
|
32
|
+
"""
|
|
33
|
+
Takes and event file and a funtional run and creates a long formatted events file
|
|
34
|
+
that maps the onset of task events to a frame of the functional run
|
|
35
|
+
|
|
36
|
+
:param bold_run: path to the function run corresponding to the event file
|
|
37
|
+
:type bold_run: pathlib.Path
|
|
38
|
+
:param event_file: path to the event timing file
|
|
39
|
+
:type event_file: pathlib.Path
|
|
40
|
+
:param output_file: file path (including name) to save the long formatted event file to
|
|
41
|
+
:type output_file: pathlib.Path
|
|
42
|
+
:param tr: Repetition time of the function run in seconds
|
|
43
|
+
:type tr: float
|
|
44
|
+
"""
|
|
45
|
+
nvols = nib.load(bold_run).dataobj.shape[-1]
|
|
46
|
+
duration = nvols * tr
|
|
47
|
+
|
|
48
|
+
events_df = pd.read_csv(event_file, index_col=None, delimiter="\t")
|
|
49
|
+
conditions = [s for s in np.unique(events_df.trial_type)]
|
|
50
|
+
events_long = pd.DataFrame(0, columns=conditions, index=np.arange(0,duration,tr))
|
|
51
|
+
|
|
52
|
+
for e in events_df.index:
|
|
53
|
+
i = find_nearest(events_long.index, events_df.loc[e, "onset"])
|
|
54
|
+
events_long.loc[i, events_df.loc[e, "trial_type"]] = 1
|
|
55
|
+
if events_df.loc[e, "duration"] > tr:
|
|
56
|
+
offset = events_df.loc[e, "onset"] + events_df.loc[e, "duration"]
|
|
57
|
+
j = find_nearest(events_long.index, offset)
|
|
58
|
+
if j>i:
|
|
59
|
+
events_long.loc[j, events_df.loc[e, "trial_type"]] = 1
|
|
60
|
+
|
|
61
|
+
events_long.to_csv(output_file)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@debug_logging
|
|
65
|
+
def append_to_confounds(confounds_file:Path, fd_thresh:float):
|
|
66
|
+
"""
|
|
67
|
+
Makes motion outlier regressors based on the framewise
|
|
68
|
+
displacement threshold, and appends them to the confounds file
|
|
69
|
+
for this functional run
|
|
70
|
+
|
|
71
|
+
:param confounds_file: path to the confounds file for this functional run
|
|
72
|
+
:type confounds_file: pathlib.Path
|
|
73
|
+
:param fd_thresh: Framewise displacement threshold for this functional run
|
|
74
|
+
:type fd_thresh: float
|
|
75
|
+
"""
|
|
76
|
+
conf_df = pd.read_csv(confounds_file, delimiter="\t")
|
|
77
|
+
b = 0
|
|
78
|
+
for a in range(len(conf_df)):
|
|
79
|
+
if conf_df.loc[a, "framewise_displacement"] > fd_thresh:
|
|
80
|
+
conf_df[f"spike{b}"] = 0
|
|
81
|
+
conf_df.loc[a, f"spike{b}"] = 1
|
|
82
|
+
b += 1
|
|
83
|
+
|
|
84
|
+
conf_df.to_csv(confounds_file, sep="\t")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@debug_logging
|
|
88
|
+
def create_events_and_confounds(bids_path:Path, derivs_path:Path, sub:str, ses:str, fd_thresh:float):
|
|
89
|
+
"""
|
|
90
|
+
Facilitates the creation of a long formatted events file
|
|
91
|
+
and the appending of motion outlier regressors to the
|
|
92
|
+
confounds file for each functional run for this subject and session
|
|
93
|
+
|
|
94
|
+
:param bids_path: Path to the bids directory containing the raw
|
|
95
|
+
:type bids_path: pathlib.Path
|
|
96
|
+
:param derivs_path: Path to BIDS-compliant derivatives folder.
|
|
97
|
+
:type derivs_path: pathlib.Path
|
|
98
|
+
:param subject: Subject id (
|
|
99
|
+
:type subject: str
|
|
100
|
+
:param session: Session id
|
|
101
|
+
:type session: str
|
|
102
|
+
:param fd_thresh: Framewise displacement threshold for this functional run
|
|
103
|
+
:type fd_thresh: float
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
log_linebreak()
|
|
107
|
+
logger.info("####### Creating long formatted event files ########\n")
|
|
108
|
+
|
|
109
|
+
bids_func = bids_path / f"sub-{sub}/ses-{ses}/func"
|
|
110
|
+
derivs_func = derivs_path / f"sub-{sub}/ses-{ses}/func"
|
|
111
|
+
if not bids_func.is_dir():
|
|
112
|
+
exit_program_early(f"Cannnot find 'func' - {bids_func} - bids directory for this subject and session")
|
|
113
|
+
if not derivs_func.is_dir():
|
|
114
|
+
exit_program_early(f"Cannnot find 'func' - {derivs_func} - derivatives directory for this subject and session")
|
|
115
|
+
|
|
116
|
+
event_time_files = list(bids_func.glob("*_events.tsv"))
|
|
117
|
+
logger.info(f"Found {len(event_time_files)} event timing files")
|
|
118
|
+
for etf in event_time_files:
|
|
119
|
+
search_path = f"/sub-{sub}_ses-{ses}*"
|
|
120
|
+
task = etf.name.split('task-')[-1].split('_')[0]
|
|
121
|
+
search_path = f"{search_path}task-{task}*"
|
|
122
|
+
run = None
|
|
123
|
+
if "run" in etf.name:
|
|
124
|
+
run = etf.name.split('run-')[-1].split('_')[0].zfill(2)
|
|
125
|
+
search_path = f"{search_path}run-{run}*"
|
|
126
|
+
bold_file = list(bids_func.glob(f"{search_path}bold.nii*"))
|
|
127
|
+
if len(bold_file) < 1:
|
|
128
|
+
logger.info(f"Could not find any bold files that matched this event timing file: {etf}")
|
|
129
|
+
continue
|
|
130
|
+
confounds_file = list(derivs_func.glob(f"{search_path}confounds_timeseries.tsv"))
|
|
131
|
+
if len(confounds_file) < 1:
|
|
132
|
+
logger.info(f"Could not find any confounds files that matched this event timing file: {etf}")
|
|
133
|
+
continue
|
|
134
|
+
confounds_file = confounds_file[0]
|
|
135
|
+
bold_file = bold_file[0]
|
|
136
|
+
side_car = bold_file.split(".")[0] + ".json"
|
|
137
|
+
tr = None
|
|
138
|
+
with open(side_car, "r") as f:
|
|
139
|
+
jd = json.load(f)
|
|
140
|
+
tr = jd["RepetitionTime"]
|
|
141
|
+
|
|
142
|
+
event_file_out = derivs_func / f"sub-{sub}_ses-{ses}_task-{task}_{f'run-{run}' if run else ''}_desc-events_long.csv"
|
|
143
|
+
make_events_long(bold_run=bold_file,
|
|
144
|
+
event_file=etf,
|
|
145
|
+
output_file=event_file_out,
|
|
146
|
+
tr=tr)
|
|
147
|
+
append_to_confounds(confounds_file=confounds_file,
|
|
148
|
+
fd_thresh=fd_thresh)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
File without changes
|