oceanproc 1.1.7__tar.gz → 1.2.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-1.1.7 → oceanproc-1.2.0}/PKG-INFO +2 -1
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/bids_wrapper.py +2 -3
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/group_series.py +96 -14
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/ocean_proc.py +97 -36
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/preprocessing_wrapper.py +86 -32
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/utils.py +1 -1
- {oceanproc-1.1.7 → oceanproc-1.2.0}/pyproject.toml +3 -3
- oceanproc-1.1.7/oceanproc/firstlevel/Ocean-Proc.code-workspace +0 -8
- {oceanproc-1.1.7 → oceanproc-1.2.0}/README.md +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/__init__.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/events_long.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/firstlevel/__init__.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/firstlevel/run_glm.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/firstlevel/tests/__init__.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/firstlevel/tests/test_run_glm.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/grouplevel/__init__.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/grouplevel/computations.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/grouplevel/design_mat_utils.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/grouplevel/dscalar_utils.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/grouplevel/utils.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/oceanparse.py +0 -0
- {oceanproc-1.1.7 → oceanproc-1.2.0}/oceanproc/segmentation_wrapper.py +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: oceanproc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: MRI preprocessing for WUSTL Ocean labs
|
|
5
5
|
Author-Email: Ramone Agard <rhagard@wustl.edu>, Joey Scanga <joeyscanga92@gmail.com>
|
|
6
6
|
Requires-Python: <3.13,>=3.10
|
|
7
|
+
Requires-Dist: sphinx-rtd-theme<3.0.0,>=2.0.0
|
|
7
8
|
Requires-Dist: flask<4.0.0,>=3.0.3
|
|
8
9
|
Requires-Dist: asttokens<3.0.0,>=2.4.1
|
|
9
10
|
Requires-Dist: numpy>=1.26.0
|
|
@@ -11,7 +11,7 @@ from textwrap import dedent
|
|
|
11
11
|
import xml.etree.ElementTree as et
|
|
12
12
|
from .utils import exit_program_early, prompt_user_continue, prepare_subprocess_logging, debug_logging, log_linebreak, flags, run_subprocess
|
|
13
13
|
import logging
|
|
14
|
-
from bids import BIDSLayout
|
|
14
|
+
from bids import BIDSLayout, BIDSLayoutIndexer
|
|
15
15
|
from bisect import bisect
|
|
16
16
|
module_logger = logging.getLogger("fsspec")
|
|
17
17
|
module_logger.setLevel(logging.CRITICAL)
|
|
@@ -23,7 +23,7 @@ remove_unusable -> relate xml and json with name and aqcuisition time (and serie
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
@debug_logging
|
|
26
|
-
def remove_unusable_runs(
|
|
26
|
+
def remove_unusable_runs(bids_layout:BIDSLayout, subject:str, session:str):
|
|
27
27
|
"""
|
|
28
28
|
Will remove unusable scans from list of scans after dcm2bids has run.
|
|
29
29
|
|
|
@@ -41,7 +41,6 @@ def remove_unusable_runs(bids_path:Path, subject:str, session:str):
|
|
|
41
41
|
logger.info("####### Removing the scans marked 'unusable' #######\n")
|
|
42
42
|
|
|
43
43
|
# Try to delete based on "quality" key in sidecar files:
|
|
44
|
-
bids_layout = BIDSLayout(bids_path, validate=False)
|
|
45
44
|
data_files = bids_layout.get(subject=subject, session=session, extension="nii.gz", datatype=".*", regex_search=True)
|
|
46
45
|
if len(data_files) == 0:
|
|
47
46
|
exit_program_early("Could not find any data files in the bids directory.")
|
|
@@ -8,7 +8,12 @@ from pathlib import Path
|
|
|
8
8
|
import xml.etree.ElementTree as et
|
|
9
9
|
from bids import BIDSLayout
|
|
10
10
|
import logging
|
|
11
|
+
|
|
12
|
+
from bids.layout import BIDSFile
|
|
11
13
|
from .utils import exit_program_early, debug_logging, log_linebreak, flags
|
|
14
|
+
import nibabel as nib
|
|
15
|
+
import numpy as np
|
|
16
|
+
import shutil
|
|
12
17
|
|
|
13
18
|
module_logger = logging.getLogger("fsspec")
|
|
14
19
|
module_logger.setLevel(logging.CRITICAL)
|
|
@@ -46,11 +51,33 @@ logger = logging.getLogger(__name__)
|
|
|
46
51
|
# return (sorted(localizers), study_id)
|
|
47
52
|
|
|
48
53
|
DEFAULT_LOCALIZER_LABEL = "00"
|
|
54
|
+
EXTRACTED_ACQ_VALUE = "bestVol"
|
|
49
55
|
|
|
50
56
|
def get_empty_group():
|
|
51
57
|
return {"task":set(), "fmapAP": set(), "fmapPA": set()}
|
|
52
58
|
|
|
53
59
|
|
|
60
|
+
def get_json_sidecar(bids_file:BIDSFile|str, bids_layout:BIDSLayout) -> BIDSFile:
|
|
61
|
+
if isinstance(bids_file, BIDSFile):
|
|
62
|
+
bids_file = bids_file.path
|
|
63
|
+
sidecar_ents = bids_layout.parse_file_entities(bids_file) | {"extension":".json"}
|
|
64
|
+
if "fmap" in sidecar_ents:
|
|
65
|
+
del sidecar_ents["fmap"]
|
|
66
|
+
sidecar_files = bids_layout.get(**sidecar_ents)
|
|
67
|
+
valid_files = [bf for bf in sidecar_files if set(sidecar_ents.keys()) == set(bids_layout.parse_file_entities(bf).keys())]
|
|
68
|
+
if len(valid_files) != 1:
|
|
69
|
+
exit_program_early(f"Cannot find the associated JSON file for bids file: {bids_file}")
|
|
70
|
+
return valid_files[0]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def reset_intended_for(fmap_file:BIDSFile|str, bids_layout:BIDSLayout):
|
|
74
|
+
sidecar_file = get_json_sidecar(fmap_file, bids_layout)
|
|
75
|
+
jd = sidecar_file.get_dict()
|
|
76
|
+
jd["IntendedFor"] = []
|
|
77
|
+
with open(sidecar_file.path, "w") as out_file:
|
|
78
|
+
json.dump(jd, out_file, indent=4)
|
|
79
|
+
|
|
80
|
+
|
|
54
81
|
def get_func_from_bids(bids_layout: BIDSLayout,
|
|
55
82
|
subject:str,
|
|
56
83
|
session:str,
|
|
@@ -78,6 +105,11 @@ def get_fmap_from_bids(bids_layout: BIDSLayout,
|
|
|
78
105
|
exit_program_early("Could not find any fieldmap files for this subject and session.")
|
|
79
106
|
|
|
80
107
|
for epi_file in fmap_files:
|
|
108
|
+
reset_intended_for(epi_file, bids_layout)
|
|
109
|
+
file_entities = bids_layout.parse_file_entities(epi_file.path)
|
|
110
|
+
# skip previously extracted acqusitions
|
|
111
|
+
if ("acquisition" in file_entities) and file_entities["acquisition"] == EXTRACTED_ACQ_VALUE:
|
|
112
|
+
continue
|
|
81
113
|
acq_time = datetime.strptime(epi_file.entities["AcquisitionTime"], "%H:%M:%S.%f")
|
|
82
114
|
direction = f"fmap{epi_file.entities['direction']}"
|
|
83
115
|
localizer_block_label = epi_file.entities["localizer_block"] if "localizer_block" in epi_file.entities else DEFAULT_LOCALIZER_LABEL
|
|
@@ -154,12 +186,12 @@ def even_fmap_pairing(localizer_group: dict) -> tuple:
|
|
|
154
186
|
@debug_logging
|
|
155
187
|
def map_fmap_to_func(subject:str,
|
|
156
188
|
session:str,
|
|
157
|
-
|
|
158
|
-
allow_uneven_fmap_groups: bool = False
|
|
189
|
+
layout:BIDSLayout,
|
|
190
|
+
allow_uneven_fmap_groups: bool = False,
|
|
191
|
+
extract_best_fmap: bool = False):
|
|
159
192
|
|
|
160
193
|
logger.info("####### Pairing field maps to functional runs #######\n")
|
|
161
194
|
|
|
162
|
-
layout = BIDSLayout(bids_path, validate=False)
|
|
163
195
|
groups = dict()
|
|
164
196
|
get_func_from_bids(bids_layout=layout,
|
|
165
197
|
subject=subject,
|
|
@@ -183,7 +215,7 @@ def map_fmap_to_func(subject:str,
|
|
|
183
215
|
if not allow_uneven_fmap_groups:
|
|
184
216
|
exit_program_early(f"--allow_uneven_fmap_groups not set. (#AP: {len(group['fmapAP'])}, #PA: {len(group['fmapPA'])})")
|
|
185
217
|
elif len(group["fmapPA"]) == 0 or len(group["fmapAP"]) == 0:
|
|
186
|
-
exit_program_early(f"Not enough fieldmaps in one direction (
|
|
218
|
+
exit_program_early(f"Not enough fieldmaps in one direction (AP:\n{group['fmapAP']}, {len(group['fmapAP'])=}\nPA:\n{group['fmapAP']}, {len(group['fmapPA'])=})")
|
|
187
219
|
fmap_pairs = uneven_fmap_pairing(group)
|
|
188
220
|
else:
|
|
189
221
|
fmap_pairs = even_fmap_pairing(group)
|
|
@@ -195,27 +227,36 @@ def map_fmap_to_func(subject:str,
|
|
|
195
227
|
fmap_times.append(times[0] + (abs(times[1] - times[0]) / 2))
|
|
196
228
|
|
|
197
229
|
# pair task runs with field maps based on closest Acquisition Time
|
|
198
|
-
|
|
230
|
+
time_pairings = {s:[] for s in fmap_pairs}
|
|
199
231
|
for (t_file, t_acq_time) in group["task"]:
|
|
200
232
|
diff = list(map(lambda x: abs(x - t_acq_time), fmap_times))
|
|
201
233
|
pairing = fmap_pairs[diff.index(min(diff))]
|
|
202
|
-
|
|
234
|
+
time_pairings[pairing].append(t_file)
|
|
235
|
+
|
|
236
|
+
# gather the sidecar files and extract volumes if requested
|
|
237
|
+
file_pairing = dict()
|
|
238
|
+
for (ap_tup, pa_tup), task_files in time_pairings.items():
|
|
239
|
+
if extract_best_fmap:
|
|
240
|
+
ap_info = extract_best_acqusition(ap_tup[0], layout)
|
|
241
|
+
pa_info = extract_best_acqusition(pa_tup[0], layout)
|
|
242
|
+
else:
|
|
243
|
+
ap_info = (ap_tup[0], get_json_sidecar(ap_tup[0], layout))
|
|
244
|
+
pa_info = (pa_tup[0], get_json_sidecar(pa_tup[0], layout))
|
|
245
|
+
file_pairing[(ap_info, pa_info)] = task_files
|
|
203
246
|
|
|
204
247
|
# add the list of task run files that are paired with each field map in their json files
|
|
205
248
|
pairing_strs = []
|
|
206
|
-
for
|
|
207
|
-
for fmap_file,
|
|
208
|
-
fmap_json = [f for f in fmap_file.get_associations() if f.entities['extension'] == ".json"]
|
|
209
|
-
if len(fmap_json) != 1:
|
|
210
|
-
exit_program_early(f"Cannot find the associated JSON file for fmap: {fmap_file.filename}")
|
|
211
|
-
fmap_json = fmap_json[0]
|
|
249
|
+
for fmap_info, task_files in file_pairing.items():
|
|
250
|
+
for fmap_file, fmap_json in fmap_info:
|
|
212
251
|
intended_for = [tf.relpath.split("/", 1)[-1] for tf in task_files]
|
|
213
252
|
task_series = set([f"{tf.filename}({str(tf.entities['SeriesNumber'])})" for tf in task_files])
|
|
214
|
-
jd =
|
|
253
|
+
jd = None
|
|
254
|
+
with open(fmap_json.path, "r") as fjson:
|
|
255
|
+
jd = json.load(fjson)
|
|
215
256
|
jd["IntendedFor"] = intended_for
|
|
216
257
|
with open(fmap_json.path, "w") as out_file:
|
|
217
258
|
logger.debug(f"writing field map pairing information to file: {fmap_json.filename}")
|
|
218
|
-
|
|
259
|
+
json.dump(jd, out_file, indent=4)
|
|
219
260
|
pairing_strs.append((f"{fmap_file.filename}:{jd['SeriesNumber']}", ' '.join(task_series)))
|
|
220
261
|
|
|
221
262
|
# report the fieldmap pairings
|
|
@@ -253,6 +294,47 @@ def map_fmap_to_func_with_pairing_file(bids_dir_path: Path,
|
|
|
253
294
|
json.dump(fmap_dict, f, indent=4)
|
|
254
295
|
|
|
255
296
|
|
|
297
|
+
def extract_best_acqusition(fmap_file: str|BIDSFile, bids_layout: BIDSLayout) -> tuple[BIDSFile]:
|
|
298
|
+
|
|
299
|
+
if isinstance(fmap_file, BIDSFile):
|
|
300
|
+
fmap_file = fmap_file.path
|
|
301
|
+
|
|
302
|
+
fmap_img = nib.load(fmap_file)
|
|
303
|
+
fmap_data = fmap_img.get_fdata()
|
|
304
|
+
assert len(fmap_data.shape) == 4, f"Fieldmap supplied does not have 4 dimensions : {fmap_file}"
|
|
305
|
+
|
|
306
|
+
# file the volume with the lowest standard deviation
|
|
307
|
+
slice_diffs = np.diff(fmap_data, axis=2)
|
|
308
|
+
vol_stds = np.nanstd(slice_diffs, axis=(0,1,2))
|
|
309
|
+
best_vol = np.argmin(vol_stds)
|
|
310
|
+
|
|
311
|
+
# create a new bids name for the best volume
|
|
312
|
+
new_entities = bids_layout.parse_file_entities(fmap_file) | {"acquisition" : EXTRACTED_ACQ_VALUE}
|
|
313
|
+
best_acq_fmap = bids_layout.build_path(new_entities)
|
|
314
|
+
# save out the new image
|
|
315
|
+
best_acq_fmap_img = fmap_img.__class__(fmap_data[:,:,:,best_vol], affine=fmap_img.affine)
|
|
316
|
+
logger.info(f"saving extracted fieldmap to path: {best_acq_fmap}")
|
|
317
|
+
nib.save(best_acq_fmap_img, best_acq_fmap)
|
|
318
|
+
|
|
319
|
+
# create a new bids name for the sidecar file
|
|
320
|
+
sidecar_entities = new_entities | {"extension": ".json"}
|
|
321
|
+
best_acq_json = bids_layout.build_path(sidecar_entities)
|
|
322
|
+
# find the original sidecar file and copy it
|
|
323
|
+
fmap_json = get_json_sidecar(fmap_file, bids_layout)
|
|
324
|
+
jd = {}
|
|
325
|
+
with open(fmap_json.path, "r") as fjs:
|
|
326
|
+
jd = json.load(fjs)
|
|
327
|
+
|
|
328
|
+
# add metadata and save
|
|
329
|
+
jd["Sources"] = ["bids::"+str(Path(fmap_json.path).relative_to(bids_layout.root))]
|
|
330
|
+
jd["ExtractedVolume"] = int(best_vol + 1)
|
|
331
|
+
with open(best_acq_json, "w") as out_js:
|
|
332
|
+
json.dump(jd, out_js, indent=4)
|
|
333
|
+
|
|
334
|
+
return (BIDSFile(best_acq_fmap), BIDSFile(best_acq_json))
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
256
338
|
if __name__ == "__main__":
|
|
257
339
|
parser = argparse.ArgumentParser(
|
|
258
340
|
prog="group_series.py",
|
|
@@ -5,13 +5,14 @@ import sys
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import logging
|
|
7
7
|
import datetime
|
|
8
|
-
from .bids_wrapper import dicom_to_bids, remove_unusable_runs, extend_session
|
|
9
|
-
from .group_series import map_fmap_to_func, map_fmap_to_func_with_pairing_file
|
|
10
|
-
from .preprocessing_wrapper import process_data, adult_defaults, infant_defaults, mount_opts
|
|
11
|
-
from .segmentation_wrapper import segmentation_args, segment_anatomical
|
|
12
|
-
from .events_long import create_events_and_confounds
|
|
13
|
-
from .utils import exit_program_early, prompt_user_continue, default_log_format, add_file_handler, export_args_to_file, flags, debug_logging, log_linebreak, extract_options, make_option
|
|
14
|
-
from .oceanparse import OceanParser
|
|
8
|
+
from oceanproc.bids_wrapper import dicom_to_bids, remove_unusable_runs, extend_session
|
|
9
|
+
from oceanproc.group_series import map_fmap_to_func, map_fmap_to_func_with_pairing_file
|
|
10
|
+
from oceanproc.preprocessing_wrapper import process_data, adult_defaults, infant_defaults, mount_opts, insert_dummy_frames
|
|
11
|
+
from oceanproc.segmentation_wrapper import segmentation_args, segment_anatomical
|
|
12
|
+
from oceanproc.events_long import create_events_and_confounds
|
|
13
|
+
from oceanproc.utils import exit_program_early, prompt_user_continue, default_log_format, add_file_handler, export_args_to_file, flags, debug_logging, log_linebreak, extract_options, make_option
|
|
14
|
+
from oceanproc.oceanparse import OceanParser
|
|
15
|
+
from bids import BIDSLayout, BIDSLayoutIndexer
|
|
15
16
|
import shutil
|
|
16
17
|
from textwrap import dedent
|
|
17
18
|
|
|
@@ -40,6 +41,31 @@ def make_work_directory(dir_path:Path, subject:str, session:str) -> Path:
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def main():
|
|
44
|
+
|
|
45
|
+
def ExistingPath(path):
|
|
46
|
+
p = Path(path)
|
|
47
|
+
if not p.exists():
|
|
48
|
+
raise argparse.ArgumentTypeError(
|
|
49
|
+
f"path string <{path}> does not represent an existing path"
|
|
50
|
+
)
|
|
51
|
+
return p.resolve()
|
|
52
|
+
|
|
53
|
+
def ExistingDir(path):
|
|
54
|
+
p = ExistingPath(path)
|
|
55
|
+
if not p.is_dir():
|
|
56
|
+
raise argparse.ArgumentTypeError(
|
|
57
|
+
f"path string <{path}> does not represent an existing directory"
|
|
58
|
+
)
|
|
59
|
+
return p
|
|
60
|
+
|
|
61
|
+
def ExistingFile(path):
|
|
62
|
+
p = ExistingPath(path)
|
|
63
|
+
if not p.is_file():
|
|
64
|
+
raise argparse.ArgumentTypeError(
|
|
65
|
+
f"path string <{path}> does not represent an existing file"
|
|
66
|
+
)
|
|
67
|
+
return p
|
|
68
|
+
|
|
43
69
|
parser = OceanParser(
|
|
44
70
|
prog="oceanproc",
|
|
45
71
|
description="Ocean Labs adult MRI preprocessing",
|
|
@@ -48,7 +74,7 @@ def main():
|
|
|
48
74
|
epilog="An arguments file can be accepted with @FILEPATH.\nAny additional arguments not listed in the help message will be passed to the preprocessing subprocess.",
|
|
49
75
|
allow_abbrev=False
|
|
50
76
|
)
|
|
51
|
-
parser.add_argument("--version", action="version", version='%(prog)s v1.1.
|
|
77
|
+
parser.add_argument("--version", action="version", version='%(prog)s v1.1.8')
|
|
52
78
|
session_args = parser.add_argument_group("Session Specific")
|
|
53
79
|
config_args = parser.add_argument_group("Configuration Arguments", "These arguments are saved to a file if the '--export_args' option is used")
|
|
54
80
|
parser.register("type", "Full-Path", lambda p: Path(p).resolve())
|
|
@@ -57,13 +83,13 @@ def main():
|
|
|
57
83
|
help="The identifier of the subject to preprocess")
|
|
58
84
|
session_args.add_argument("--session", "-se", required=True,
|
|
59
85
|
help="The identifier of the session to preprocess")
|
|
60
|
-
session_args.add_argument("--source_data", "-sd", type=
|
|
86
|
+
session_args.add_argument("--source_data", "-sd", type=ExistingDir,
|
|
61
87
|
help="The path to the directory containing the raw DICOM files for this subject and session")
|
|
62
88
|
session_args.add_argument("--skip_dcm2bids", action="store_true",
|
|
63
89
|
help="Flag to indicate that dcm2bids does not need to be run for this subject and session")
|
|
64
|
-
session_args.add_argument("--usability_file", "-u", type=
|
|
90
|
+
session_args.add_argument("--usability_file", "-u", type=ExistingFile,
|
|
65
91
|
help="The path to the usability file for this subject and session; this file can be either be an xml or json file. All runs will be used if no file is provided.")
|
|
66
|
-
session_args.add_argument("--longitudinal", "-lg", type=
|
|
92
|
+
session_args.add_argument("--longitudinal", "-lg", type=ExistingPath, nargs="+", action="append",
|
|
67
93
|
help="Addtional sourcedata and usability pair to include with this BIDs subject and session")
|
|
68
94
|
session_args.add_argument("--skip_fmap_pairing", action="store_true",
|
|
69
95
|
help="Flag to indicate that the pairing of fieldmaps to BOLD runs does not need to be performed for this subject and session")
|
|
@@ -79,19 +105,19 @@ def main():
|
|
|
79
105
|
help="Flag to stop the deletion of the fMRIPrep working directory")
|
|
80
106
|
session_args.add_argument("--debug_mode", action="store_true",
|
|
81
107
|
help="Flag to run the program in debug mode for more verbose logging")
|
|
82
|
-
session_args.add_argument("--fmap_pairing_file", type=
|
|
108
|
+
session_args.add_argument("--fmap_pairing_file", type=ExistingFile,
|
|
83
109
|
help="Path to JSON containing info on how to pair fieldmaps to BOLD runs.")
|
|
84
110
|
|
|
85
|
-
config_args.add_argument("--bids_path", "-b", type=
|
|
111
|
+
config_args.add_argument("--bids_path", "-b", type=ExistingDir, required=True,
|
|
86
112
|
help="The path to the directory containing the raw nifti data for all subjects, in BIDS format")
|
|
87
|
-
config_args.add_argument("--derivs_path", "-d", type=
|
|
113
|
+
config_args.add_argument("--derivs_path", "-d", type=ExistingDir, required=True,
|
|
88
114
|
help="The path to the BIDS formated derivatives directory for this subject")
|
|
89
115
|
config_args.add_argument("--derivs_subfolder", "-ds", default=None,
|
|
90
116
|
help=f"""The subfolder in the derivatives directory where bids style outputs should be stored.
|
|
91
117
|
The default is {adult_defaults.derivs_subfolder} or {infant_defaults.derivs_subfolder} if the '--infant' flag is set.""")
|
|
92
|
-
config_args.add_argument("--bids_config", "-c", type=
|
|
118
|
+
config_args.add_argument("--bids_config", "-c", type=ExistingFile, required=True,
|
|
93
119
|
help="The path to the dcm2bids config file to use for this subject and session")
|
|
94
|
-
config_args.add_argument("--nordic_config", "-n", type=
|
|
120
|
+
config_args.add_argument("--nordic_config", "-n", type=ExistingFile,
|
|
95
121
|
help="The path to the second dcm2bids config file to use for this subject and session. This implies that the session contains NORDIC data")
|
|
96
122
|
config_args.add_argument("--nifti", action=argparse.BooleanOptionalAction,
|
|
97
123
|
help="Flag to specify that the source directory contains files of type NIFTI (.nii/.jsons) instead of DICOM")
|
|
@@ -101,36 +127,43 @@ def main():
|
|
|
101
127
|
help="framewise displacement threshold (in mm) to determine outlier framee (Default is 0.9).")
|
|
102
128
|
config_args.add_argument("--skip_bids_validation", action=argparse.BooleanOptionalAction,
|
|
103
129
|
help="Specifies skipping BIDS validation (only enabled for fMRIprep step)")
|
|
104
|
-
config_args.add_argument("--fs_subjects_dir", "-fs", type=
|
|
130
|
+
config_args.add_argument("--fs_subjects_dir", "-fs", type=ExistingDir,
|
|
105
131
|
help="The path to the directory that contains previous FreeSurfer outputs/derivatives to use for fMRIPrep. If empty, this is the path where new FreeSurfer outputs will be stored.")
|
|
106
132
|
config_args.add_argument("--allow_uneven_fmap_groups", action="store_true",
|
|
107
133
|
help="Flag to allow for automated fieldmap pairing when there's more AP- than PA-encoded fieldmaps, or vice versa (will still error out if at least one of each is not present.)")
|
|
134
|
+
config_args.add_argument("--extract_best_fmap", action="store_true",
|
|
135
|
+
help="Flag to specify that the lowest variance fieldmap volume should be extracted from each fieldmap, and used for distortion correction (opposed to all volumes being used)")
|
|
136
|
+
config_args.add_argument("--dscans_path", type=ExistingDir,
|
|
137
|
+
help="""The path to the directory containing 'dscans' files for inserting dummy scans into bold runs. Must be named exactly as corresponding bold image except with 'dscans' as
|
|
138
|
+
the suffix and '.tsv' as the extension. Contents of the file must be a column, titled 'dummy_scan', with ones and zeros. Ones denote which frames should be dummy scans.""")
|
|
108
139
|
config_args.add_argument("--precomputed_derivatives", "-pd", type="Full-Path", dest="derivatives", nargs="*",
|
|
109
140
|
help="A list of paths to any BIDS-style precomputed derivatives that should be used in preprocessing. (Ex. /path/to/bibsnet)")
|
|
110
|
-
config_args.add_argument("--work_dir", "-w", type=
|
|
141
|
+
config_args.add_argument("--work_dir", "-w", type=ExistingDir, required=True,
|
|
111
142
|
help="The path to the working directory used to store intermediate files")
|
|
112
|
-
config_args.add_argument("--fs_license", "-l", type=
|
|
143
|
+
config_args.add_argument("--fs_license", "-l", type=ExistingFile, required=True,
|
|
113
144
|
help="The path to the license file for the local installation of FreeSurfer")
|
|
114
145
|
config_args.add_argument("--image_version", "-iv", default=None,
|
|
115
146
|
help=f"""The version of fmriprep to use; It is reccomended that an entire study use the same version.
|
|
116
147
|
The default is {adult_defaults.image_version} or {infant_defaults.image_version} if the '--infant' flag is set.""")
|
|
117
148
|
config_args.add_argument("--infant", "-I", action=argparse.BooleanOptionalAction,
|
|
118
149
|
help="Flag to specify that NiBabies should be used instead of fMRIPrep")
|
|
119
|
-
config_args.add_argument("--
|
|
150
|
+
config_args.add_argument("--preproc_image_path", "-pi", type=ExistingFile,
|
|
151
|
+
help="Path to a fMRIPrep or NiBabies apptainer image to use for preprocessing. If provided, this apptainer image will be used instead of docker. (NiBabies images should still be used with the '--infant' flag)")
|
|
152
|
+
config_args.add_argument("--bibsnet_image_path", "-bi", type=ExistingFile,
|
|
120
153
|
help="Path to the BIBSnet apptainer image to use for segmentation. If provided, BIBSnet segmentation will be run and the outputs will be used for preprocessing. (Must be used with the '--infant' flag)")
|
|
121
|
-
config_args.add_argument("--bibsnet_work", "-bw", type=
|
|
154
|
+
config_args.add_argument("--bibsnet_work", "-bw", type=ExistingDir,
|
|
122
155
|
help="The path to the working directory used to store intermediate files for BIBSnet")
|
|
123
156
|
args, unknown_args = parser.parse_known_args()
|
|
124
157
|
|
|
125
|
-
try:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
except AssertionError as e:
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
# try:
|
|
159
|
+
# assert args.derivs_path.is_dir(), "Derivatives directory must exist but it cannot be found"
|
|
160
|
+
# assert args.bids_path.is_dir(), "Raw Bids directory must exist but it cannot be found"
|
|
161
|
+
# if not args.skip_dcm2bids:
|
|
162
|
+
# assert args.source_data.is_dir(), "Source data directory must exist but it cannot be found"
|
|
163
|
+
# assert args.work_dir.is_dir(), "Work directroy must exist but it cannot be found"
|
|
164
|
+
# except AssertionError as e:
|
|
165
|
+
# logger.exception(e)
|
|
166
|
+
# parser.error(e)
|
|
134
167
|
|
|
135
168
|
defaults = infant_defaults if args.infant else adult_defaults
|
|
136
169
|
for k,v in defaults.__dict__.items():
|
|
@@ -146,8 +179,8 @@ def main():
|
|
|
146
179
|
elif bibsnet_path not in args.derivatives:
|
|
147
180
|
args.derivatives.append(bibsnet_path)
|
|
148
181
|
|
|
149
|
-
if args.bibsnet_work and (not args.bibsnet_work.exists()):
|
|
150
|
-
|
|
182
|
+
# if args.bibsnet_work and (not args.bibsnet_work.exists()):
|
|
183
|
+
# parser.error("BIBSnet working directory must exist but it cannot be found")
|
|
151
184
|
|
|
152
185
|
if args.longitudinal:
|
|
153
186
|
for lg_group in args.longitudinal:
|
|
@@ -162,6 +195,12 @@ def main():
|
|
|
162
195
|
parser.error(f"longitudinal argument cannot have more than 2 elements: {lg_group}")
|
|
163
196
|
|
|
164
197
|
preproc_image = f"{defaults.image_name}:{args.image_version}"
|
|
198
|
+
if args.preproc_image_path:
|
|
199
|
+
# if not args.preproc_image_path.exists():
|
|
200
|
+
# parser.error(f"Cannot find the preprocessing apptainer image at the path: {args.preproc_image_path}")
|
|
201
|
+
flags.apptainer = True
|
|
202
|
+
preproc_image = args.preproc_image_path
|
|
203
|
+
|
|
165
204
|
preproc_derivs_path = args.derivs_path / args.derivs_subfolder
|
|
166
205
|
|
|
167
206
|
log_dir = preproc_derivs_path / f"sub-{args.subject}/log"
|
|
@@ -180,7 +219,7 @@ def main():
|
|
|
180
219
|
try:
|
|
181
220
|
assert args.export_args.parent.exists() and args.export_args.suffix, "Argument export path must be a file path in a directory that exists"
|
|
182
221
|
logger.info(f"####### Exporting Configuration Arguments to: '{args.export_args}' #######")
|
|
183
|
-
export_args_to_file(args,
|
|
222
|
+
export_args_to_file(args, config_args, args.export_args, extra_args=unknown_args)
|
|
184
223
|
except Exception as e:
|
|
185
224
|
logger.exception(e)
|
|
186
225
|
exit_program_early(e)
|
|
@@ -226,10 +265,18 @@ def main():
|
|
|
226
265
|
session=args.session,
|
|
227
266
|
tmp_dir=bids_path,
|
|
228
267
|
bids_dir=args.bids_path)
|
|
268
|
+
|
|
269
|
+
# create a bids layout object
|
|
270
|
+
ignore_list = ["tmp_dcm2bids"]
|
|
271
|
+
if (bids_path / ".bidsignore").is_file():
|
|
272
|
+
with open(bids_path / ".bidsignore", "r") as f:
|
|
273
|
+
ignore_list.extend(f.readlines())
|
|
274
|
+
ignore_list = list(set(ignore_list))
|
|
275
|
+
bids_layout = BIDSLayout(args.bids_path, validate=False, indexer=BIDSLayoutIndexer(ignore=ignore_list, validate=False))
|
|
229
276
|
|
|
230
277
|
##### Remove the scans marked as 'unusable' #####
|
|
231
278
|
remove_unusable_runs(
|
|
232
|
-
|
|
279
|
+
bids_layout=bids_layout,
|
|
233
280
|
subject=args.subject,
|
|
234
281
|
session=args.session
|
|
235
282
|
)
|
|
@@ -243,12 +290,15 @@ def main():
|
|
|
243
290
|
args.fmap_pairing_file
|
|
244
291
|
)
|
|
245
292
|
else:
|
|
293
|
+
# update the bids layout
|
|
294
|
+
bids_layout = BIDSLayout(args.bids_path, validate=False, indexer=BIDSLayoutIndexer(ignore=ignore_list, validate=False))
|
|
246
295
|
map_fmap_to_func(
|
|
247
296
|
subject=args.subject,
|
|
248
297
|
session=args.session,
|
|
249
|
-
|
|
298
|
+
layout=bids_layout,
|
|
250
299
|
# xml_path=xml_data_path,
|
|
251
|
-
allow_uneven_fmap_groups=args.allow_uneven_fmap_groups
|
|
300
|
+
allow_uneven_fmap_groups=args.allow_uneven_fmap_groups,
|
|
301
|
+
extract_best_fmap=args.extract_best_fmap
|
|
252
302
|
)
|
|
253
303
|
|
|
254
304
|
##### Run BIBSnet #####
|
|
@@ -272,6 +322,17 @@ def main():
|
|
|
272
322
|
if not bibsnet_path.exists():
|
|
273
323
|
exit_program_early(f"Cannot find the outputs for BIBSnet at the path {bibsnet_path}")
|
|
274
324
|
|
|
325
|
+
|
|
326
|
+
##### Insert dummy scans in requested #####
|
|
327
|
+
if args.dscans_path:
|
|
328
|
+
insert_dummy_frames(
|
|
329
|
+
subject=args.subject,
|
|
330
|
+
session=args.session,
|
|
331
|
+
dscan_dir=args.dscans_path,
|
|
332
|
+
bids_layout=bids_layout
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
275
336
|
##### Run fMRIPrep or NiBabies #####
|
|
276
337
|
all_opts = dict(args._get_kwargs())
|
|
277
338
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
import logging
|
|
5
|
+
from bids import BIDSLayout
|
|
5
6
|
from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess
|
|
6
7
|
import shutil
|
|
7
8
|
from subprocess import Popen, PIPE
|
|
@@ -9,6 +10,7 @@ from matplotlib import pyplot as plt
|
|
|
9
10
|
from bs4 import BeautifulSoup as bsoup
|
|
10
11
|
import pandas as pd
|
|
11
12
|
import numpy as np
|
|
13
|
+
import nibabel as nib
|
|
12
14
|
import json
|
|
13
15
|
import os
|
|
14
16
|
import copy
|
|
@@ -29,14 +31,6 @@ infant_defaults = SimpleNamespace(
|
|
|
29
31
|
derivs_subfolder="nibabies",
|
|
30
32
|
)
|
|
31
33
|
|
|
32
|
-
# Good arguments to have if not specified
|
|
33
|
-
# { key : (val, can_have_multiple_values)}
|
|
34
|
-
preproc_kwargs = {
|
|
35
|
-
"cifti-output":("91k", False),
|
|
36
|
-
"output-spaces": ("fsLR", True),
|
|
37
|
-
"output-spaces": ("MNI152NLin2009cAsym", True)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
34
|
# Preprocessing arguments that need to have their paths binded when running docker
|
|
41
35
|
mount_opts = {"fs_subjects_dir",
|
|
42
36
|
"derivatives"}
|
|
@@ -83,6 +77,15 @@ def run_preprocessing(subject:str,
|
|
|
83
77
|
elif not derivs_path.exists():
|
|
84
78
|
exit_program_early(f"Derivatives path {derivs_path} does not exist.")
|
|
85
79
|
|
|
80
|
+
if flags.apptainer:
|
|
81
|
+
cmd_prelude = "apptainer run --nv --cleanenv --no-mount cwd"
|
|
82
|
+
mount_flag = "-B"
|
|
83
|
+
else:
|
|
84
|
+
uid = Popen(["id", "-u"], stdout=PIPE).stdout.read().decode("utf-8").strip()
|
|
85
|
+
gid = Popen(["id", "-g"], stdout=PIPE).stdout.read().decode("utf-8").strip()
|
|
86
|
+
cmd_prelude = f"docker run --rm -i -u {uid}:{gid}"
|
|
87
|
+
mount_flag = "-v"
|
|
88
|
+
|
|
86
89
|
additional_mount_paths = []
|
|
87
90
|
additional_mount_args = []
|
|
88
91
|
mount_dex = 1
|
|
@@ -90,29 +93,26 @@ def run_preprocessing(subject:str,
|
|
|
90
93
|
arg_vals = [k]
|
|
91
94
|
if isinstance(v, list):
|
|
92
95
|
for sub_v in v:
|
|
93
|
-
additional_mount_paths.append(f"
|
|
96
|
+
additional_mount_paths.append(f"{mount_flag} {sub_v.resolve()}:/deriv{mount_dex}")
|
|
94
97
|
arg_vals.append(f"/deriv{mount_dex}")
|
|
95
98
|
mount_dex += 1
|
|
96
99
|
else:
|
|
97
|
-
additional_mount_paths.append(f"
|
|
100
|
+
additional_mount_paths.append(f"{mount_flag} {v.resolve()}:/deriv{mount_dex}")
|
|
98
101
|
arg_vals.append(f"/deriv{mount_dex}")
|
|
99
102
|
mount_dex += 1
|
|
100
103
|
additional_mount_args.append(" ".join(arg_vals))
|
|
101
104
|
additional_mount_paths = " ".join(additional_mount_paths)
|
|
102
105
|
additional_mount_args = " ".join(additional_mount_args)
|
|
103
106
|
|
|
104
|
-
uid = Popen(["id", "-u"], stdout=PIPE).stdout.read().decode("utf-8").strip()
|
|
105
|
-
gid = Popen(["id", "-g"], stdout=PIPE).stdout.read().decode("utf-8").strip()
|
|
106
|
-
|
|
107
107
|
license_mount = "/opt/freesurfer/license.txt"
|
|
108
108
|
bids_mount = "/data"
|
|
109
109
|
derivs_mount = "/out"
|
|
110
110
|
work_mount = "/work"
|
|
111
|
-
preproc_command = f"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
preproc_command = f"""{cmd_prelude}
|
|
112
|
+
{mount_flag} {license_file.resolve()}:{license_mount}:ro
|
|
113
|
+
{mount_flag} {bids_path.resolve()}:{bids_mount}:ro
|
|
114
|
+
{mount_flag} {derivs_path.resolve()}:{derivs_mount}
|
|
115
|
+
{mount_flag} {work_path.resolve()}:{work_mount}
|
|
116
116
|
{additional_mount_paths}
|
|
117
117
|
{image_name} {bids_mount} {derivs_mount} participant
|
|
118
118
|
--participant-label {subject}
|
|
@@ -240,6 +240,74 @@ def add_fd_plot_to_report(subject:str,
|
|
|
240
240
|
f.write(soup.prettify())
|
|
241
241
|
|
|
242
242
|
|
|
243
|
+
def insert_dummy_frames(subject:str,
|
|
244
|
+
session:str,
|
|
245
|
+
dscan_dir:Path,
|
|
246
|
+
bids_layout:BIDSLayout):
|
|
247
|
+
|
|
248
|
+
dscans_suffix = "dscans"
|
|
249
|
+
dscans_path_pattern = "sub-{subject}[/ses-{session}]/{datatype<func|meg|beh>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_recording-{recording}]_{suffix<"+dscans_suffix+">}{extension<.tsv|.json>|.tsv}"
|
|
250
|
+
|
|
251
|
+
log_linebreak()
|
|
252
|
+
logger.info(f"####### Inserting Dummy Scans #######\n")
|
|
253
|
+
|
|
254
|
+
# find all bold files
|
|
255
|
+
func_files = bids_layout.get(subject=subject, session=session, suffix="bold", datatype="func", extension="nii.gz")
|
|
256
|
+
for bfile in func_files:
|
|
257
|
+
if not Path(bfile.path).exists:
|
|
258
|
+
continue
|
|
259
|
+
# Find the dscans file for bold run
|
|
260
|
+
dscans_entities = bids_layout.parse_file_entities(bfile.path) | {"suffix":dscans_suffix, "extension":".tsv"}
|
|
261
|
+
dscans_bids_path = bids_layout.build_path(dscans_entities, path_patterns=[dscans_path_pattern], validate=False)
|
|
262
|
+
dscans_file = sorted(dscan_dir.glob(Path(dscans_bids_path).name))
|
|
263
|
+
if len(dscans_file) > 1:
|
|
264
|
+
exit_program_early(f"Found more than one dscans file for bold run {bfile} --> {dscans_file}")
|
|
265
|
+
elif len(dscans_file) == 0:
|
|
266
|
+
logger.info(f"Did not find any dscans files for bold run: {bfile}, file will remain unmodified.")
|
|
267
|
+
continue
|
|
268
|
+
dscans_file = dscans_file[0]
|
|
269
|
+
logger.info(f"found dscans file <{dscans_file.name}> for bold run <{bfile.filename}>")
|
|
270
|
+
|
|
271
|
+
dscans_list = pd.read_csv(dscans_file, sep="\t").loc[:, "dummy_scan"].to_numpy().astype(np.int32)
|
|
272
|
+
bold_img = nib.load(bfile.path)
|
|
273
|
+
num_bold_frames = bold_img.shape[3]
|
|
274
|
+
num_dummy_scans = np.sum(dscans_list > 0)
|
|
275
|
+
num_non_dummy = np.sum(dscans_list == 0)
|
|
276
|
+
|
|
277
|
+
# validate length
|
|
278
|
+
if len(dscans_list) == num_bold_frames:
|
|
279
|
+
logger.info(f" dsans file and bold image have the same length. Dummy scans may have already been inserted or length of dscans file is incorrect. Bold file will remain unmodified.")
|
|
280
|
+
continue
|
|
281
|
+
elif num_bold_frames != num_non_dummy:
|
|
282
|
+
exit_program_early(f"Number of non-dummy scans and length of bold data do not match. # non-dummy scans: {num_non_dummy}, # bold frames: {num_bold_frames}")
|
|
283
|
+
|
|
284
|
+
logger.info(f" inserting {num_dummy_scans} dummy scans into the bold run")
|
|
285
|
+
# create a new bold series with dummy scans inserted
|
|
286
|
+
bold_data = bold_img.get_fdata()
|
|
287
|
+
frame_list = []
|
|
288
|
+
dscans_index = 0
|
|
289
|
+
for f in range(num_bold_frames):
|
|
290
|
+
copy_dex = f-1 if f > 0 else f
|
|
291
|
+
while dscans_list[dscans_index] != 0:
|
|
292
|
+
frame_list.append(bold_data[:,:,:,copy_dex])
|
|
293
|
+
dscans_index += 1
|
|
294
|
+
frame_list.append(bold_data[:,:,:,f])
|
|
295
|
+
dscans_index += 1
|
|
296
|
+
# check if end of run needs to be extended
|
|
297
|
+
if (f == num_bold_frames-1) and (dscans_index < len(dscans_list)):
|
|
298
|
+
while dscans_index < len(dscans_list):
|
|
299
|
+
frame_list.append(bold_data[:,:,:,f])
|
|
300
|
+
dscans_index += 1
|
|
301
|
+
|
|
302
|
+
if len(frame_list) != len(dscans_list):
|
|
303
|
+
exit_program_early(f"Something went wrong when creating dummy scans, lengths do not match. bold frames: {len(frame_list)}, dscan length: {len(dscans_list)}")
|
|
304
|
+
|
|
305
|
+
# save new image
|
|
306
|
+
extended_bold_data = np.stack(frame_list, axis=3)
|
|
307
|
+
extended_bold_img = bold_img.__class__(extended_bold_data, header=bold_img.header, affine=bold_img.affine)
|
|
308
|
+
logger.info(f" saving extended bold image")
|
|
309
|
+
nib.save(extended_bold_img, bfile.path)
|
|
310
|
+
|
|
243
311
|
|
|
244
312
|
@debug_logging
|
|
245
313
|
def process_data(subject:str,
|
|
@@ -273,20 +341,6 @@ def process_data(subject:str,
|
|
|
273
341
|
:type remove_work_folder: str
|
|
274
342
|
:param **kwargs: any arguments to be passed to the preprocessing subprocess
|
|
275
343
|
"""
|
|
276
|
-
|
|
277
|
-
# Add in some recommended options
|
|
278
|
-
for key, (val, multi) in preproc_kwargs.items():
|
|
279
|
-
if (key in kwargs) and multi:
|
|
280
|
-
if isinstance(kwargs[key], list):
|
|
281
|
-
if val not in kwargs[key]:
|
|
282
|
-
kwargs[key].append(val)
|
|
283
|
-
else:
|
|
284
|
-
if val != kwargs[key]:
|
|
285
|
-
kwargs[key] = [kwargs[key], val]
|
|
286
|
-
elif key not in kwargs:
|
|
287
|
-
kwargs[key] = val
|
|
288
|
-
|
|
289
|
-
|
|
290
344
|
option_chain = " ".join([make_option(v, key=k, delimeter=" ", convert_underscore=True) for k,v in kwargs.items()])
|
|
291
345
|
preproc_title = "NiBabies" if flags.infant else "fMRIPrep"
|
|
292
346
|
|
|
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|
|
16
16
|
|
|
17
17
|
default_log_format = "%(levelname)s:%(asctime)s:%(module)s: %(message)s"
|
|
18
18
|
|
|
19
|
-
flags = SimpleNamespace(debug=False, longitudinal=False, infant=False)
|
|
19
|
+
flags = SimpleNamespace(debug=False, longitudinal=False, infant=False, apptainer=False)
|
|
20
20
|
|
|
21
21
|
bids_entities = ["sub", "ses", "sample", "task", "tracksys",
|
|
22
22
|
"acq", "ce", "trc", "stain", "rec", "dir",
|
|
@@ -22,6 +22,7 @@ authors = [
|
|
|
22
22
|
]
|
|
23
23
|
requires-python = "<3.13,>=3.10"
|
|
24
24
|
dependencies = [
|
|
25
|
+
"sphinx-rtd-theme>=2.0.0,<3.0.0",
|
|
25
26
|
"flask<4.0.0,>=3.0.3",
|
|
26
27
|
"asttokens<3.0.0,>=2.4.1",
|
|
27
28
|
"numpy>=1.26.0",
|
|
@@ -33,13 +34,13 @@ dependencies = [
|
|
|
33
34
|
"pybids>=0.16.3",
|
|
34
35
|
]
|
|
35
36
|
name = "oceanproc"
|
|
36
|
-
version = "1.
|
|
37
|
+
version = "1.2.0"
|
|
37
38
|
description = "MRI preprocessing for WUSTL Ocean labs"
|
|
38
39
|
readme = "README.md"
|
|
39
40
|
|
|
40
41
|
[project.scripts]
|
|
41
42
|
oceanproc = "oceanproc.ocean_proc:main"
|
|
42
|
-
|
|
43
|
+
oceanfla_legacy = "oceanproc.firstlevel.run_glm:main"
|
|
43
44
|
|
|
44
45
|
[dependency-groups]
|
|
45
46
|
dev = [
|
|
@@ -47,5 +48,4 @@ dev = [
|
|
|
47
48
|
"sphinx<8.0.0,>=7.3.7",
|
|
48
49
|
"twine<6.0.0,>=5.1.0",
|
|
49
50
|
"ipython<9.0.0,>=8.29.0",
|
|
50
|
-
"sphinx-rtd-theme>=2.0.0,<3.0.0",
|
|
51
51
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|