cnda_dl 1.0.1__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.
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/PKG-INFO +1 -1
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/cnda_dl/cli.py +181 -198
- cnda_dl-1.2.0/cnda_dl/formatters.py +65 -0
- cnda_dl-1.2.0/cnda_dl/zip_utils.py +28 -0
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/pyproject.toml +2 -1
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/LICENSE +0 -0
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/README.md +0 -0
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/cnda_dl/.ruff_cache/.gitignore +0 -0
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/cnda_dl/.ruff_cache/0.9.9/15962950311086395899 +0 -0
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/cnda_dl/.ruff_cache/CACHEDIR.TAG +0 -0
- {cnda_dl-1.0.1 → cnda_dl-1.2.0}/cnda_dl/__init__.py +0 -0
@@ -1,32 +1,32 @@
|
|
1
|
-
#!/usr/bin/env python
|
2
|
-
|
3
1
|
'''
|
4
2
|
Script to download MRI sessions from the CNDA
|
5
3
|
Authors:
|
6
4
|
Joey Scanga (scanga@wustl.edu)
|
7
5
|
Ramone Agard (rhagard@wustl.edu)
|
8
6
|
'''
|
9
|
-
|
10
7
|
from glob import glob
|
11
|
-
from matplotlib.ticker import EngFormatter
|
12
8
|
from pathlib import Path
|
13
|
-
|
14
|
-
import
|
9
|
+
import atexit
|
10
|
+
import re
|
15
11
|
import argparse
|
16
12
|
import logging
|
17
13
|
import os
|
18
|
-
import progressbar
|
19
14
|
import shlex
|
20
15
|
import shutil
|
21
|
-
import hashlib
|
22
16
|
import subprocess
|
23
17
|
import sys
|
24
18
|
import xml.etree.ElementTree as et
|
25
|
-
import zipfile
|
26
19
|
import datetime
|
27
20
|
|
21
|
+
import pyxnat as px
|
22
|
+
import progressbar as pb
|
23
|
+
|
24
|
+
from .formatters import ParensOnRightFormatter1
|
25
|
+
from .zip_utils import recursive_unzip, unzipped
|
26
|
+
|
28
27
|
default_log_format = "%(levelname)s:%(funcName)s: %(message)s"
|
29
28
|
sout_handler = logging.StreamHandler(stream=sys.stdout)
|
29
|
+
sout_handler.setFormatter(ParensOnRightFormatter1())
|
30
30
|
logging.basicConfig(level=logging.INFO,
|
31
31
|
handlers=[sout_handler],
|
32
32
|
format=default_log_format)
|
@@ -49,34 +49,34 @@ def handle_dir_creation(dir_path: Path):
|
|
49
49
|
ans = ans.lower()
|
50
50
|
|
51
51
|
if len(ans) != 1 or ans not in 'yn':
|
52
|
-
logger.info("Invalid response
|
52
|
+
logger.info("Invalid response")
|
53
53
|
elif ans == 'y':
|
54
54
|
dir_path.mkdir(parents=True)
|
55
55
|
prompt_chosen = True
|
56
|
-
logger.info(f"new directory created at {dir_path}
|
56
|
+
logger.info(f"new directory created at {dir_path}")
|
57
57
|
elif ans == 'n':
|
58
|
-
logger.info("Chose to not create a new directory
|
58
|
+
logger.info("Chose to not create a new directory Aborting")
|
59
59
|
sys.exit(0)
|
60
60
|
else:
|
61
|
-
logger.info("Invalid response
|
61
|
+
logger.info("Invalid response")
|
62
62
|
|
63
63
|
|
64
|
-
def download_xml(central: Interface,
|
64
|
+
def download_xml(central: px.Interface,
|
65
65
|
subject_id: str,
|
66
66
|
project_id: str,
|
67
67
|
file_path: Path):
|
68
68
|
|
69
|
-
logger.info("Downloading session xml
|
69
|
+
logger.info("Downloading session xml")
|
70
70
|
sub = central.select(f"/projects/{project_id}/subjects/{subject_id}")
|
71
71
|
with open(file_path, "w") as f:
|
72
72
|
f.write(sub.get().decode())
|
73
73
|
return True
|
74
74
|
|
75
75
|
|
76
|
-
def retrieve_experiment(central: Interface,
|
76
|
+
def retrieve_experiment(central: px.Interface,
|
77
77
|
session: str,
|
78
78
|
experiment_id: bool = False,
|
79
|
-
project_id: str = None) ->
|
79
|
+
project_id: str = None) -> px.jsonutil.JsonTable:
|
80
80
|
|
81
81
|
query_params = {}
|
82
82
|
if project_id:
|
@@ -89,148 +89,147 @@ def retrieve_experiment(central: Interface,
|
|
89
89
|
return central.array.mrsessions(**query_params)
|
90
90
|
|
91
91
|
|
92
|
-
def get_xml_scans(xml_file: Path
|
93
|
-
|
92
|
+
def get_xml_scans(xml_file: Path) -> dict:
|
93
|
+
"""
|
94
|
+
Create a map of downloaded scan IDs to UIDs to later match with the UIDs in the .dat files
|
94
95
|
|
96
|
+
:param xml_file: path to quality XML
|
97
|
+
:type xml_file: pathlib.Path
|
98
|
+
"""
|
95
99
|
xml_tree = et.parse(xml_file)
|
96
100
|
prefix = "{" + str(xml_tree.getroot()).split("{")[-1].split("}")[0] + "}"
|
97
101
|
scan_xml_entries = xml_tree.getroot().find(
|
98
102
|
f"./{prefix}experiments/{prefix}experiment/{prefix}scans"
|
99
103
|
)
|
100
|
-
if quality_pair:
|
101
|
-
return {s.get("ID"): s.find(f"{prefix}quality").text
|
102
|
-
for s in scan_xml_entries}
|
103
104
|
return scan_xml_entries
|
104
105
|
|
105
106
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
#
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
107
|
+
def get_scan_types(xml_path):
|
108
|
+
# Get unique scan types to include in POST req
|
109
|
+
with open(xml_path, "r") as f:
|
110
|
+
xml_text = f.read()
|
111
|
+
return list(set(
|
112
|
+
re.findall(
|
113
|
+
r'ID="[\d\w_\-]+"\s+type="([a-zA-Z0-9\-_\.]+)"',
|
114
|
+
xml_text
|
115
|
+
)
|
116
|
+
))
|
117
|
+
|
118
|
+
|
119
|
+
def get_resources(xml_path):
|
120
|
+
# Get "additional resources" that appear on CNDA for the session
|
121
|
+
# (usually NORDIC_VOLUMES)
|
122
|
+
with open(xml_path, "r") as f:
|
123
|
+
xml_text = f.read()
|
124
|
+
return list(set(
|
125
|
+
re.findall(
|
126
|
+
r'resource label="([a-zA-Z0-9\-_]+)"',
|
127
|
+
xml_text
|
128
|
+
)
|
129
|
+
))
|
130
|
+
|
131
|
+
|
132
|
+
def download_experiment_zip(central: px.Interface,
|
133
|
+
exp: px.jsonutil.JsonTable,
|
134
|
+
dicom_dir: Path,
|
135
|
+
xml_file_path: Path,
|
136
|
+
keep_zip: bool = False):
|
137
|
+
'''
|
138
|
+
Download scan data as .zip from CNDA.
|
139
|
+
|
140
|
+
:param central: CNDA connection object
|
141
|
+
:type central: pyxnat.Interface
|
142
|
+
:param exp: object containing experiment information
|
143
|
+
:type exp: pyxnat.jsonutil.JsonTable
|
144
|
+
:param dicom_dir: Path to session-specific directory where DICOMs should be downloaded
|
145
|
+
:type dicom_dir: pathlib.Path
|
146
|
+
:param xml_file_path: Path to experiment XML
|
147
|
+
:type xml_file_path: pathlib.Path
|
148
|
+
:param keep_zip: Will not delete downloaded zip file after unzipping
|
149
|
+
:type keep_zip: bool
|
150
|
+
'''
|
151
|
+
sub_obj = central.select(f"/project/{exp['project']}/subjects/{exp['xnat:mrsessiondata/subject_id']}")
|
152
|
+
exp_obj = central.select(f"/project/{exp['project']}/subjects/{exp['xnat:mrsessiondata/subject_id']}/experiments/{exp['ID']}")
|
153
|
+
# Step 1: make POST request to prepare .zip download
|
154
|
+
res1 = central.post(
|
155
|
+
"/xapi/archive/downloadwithsize",
|
156
|
+
json={
|
157
|
+
"sessions": [f"{exp['project']}:{sub_obj.label()}:{exp_obj.label()}:{exp['ID']}"],
|
158
|
+
"projectIds": [exp['project']],
|
159
|
+
"scan_formats": ["DICOM"],
|
160
|
+
"scan_types": get_scan_types(xml_file_path),
|
161
|
+
"resources": get_resources(xml_file_path),
|
162
|
+
"options": ["simplified"]
|
163
|
+
}
|
164
|
+
)
|
165
|
+
# Step 2: make GET request with created ID from POST
|
166
|
+
cur_bytes, total_bytes = 0, int(res1.json()["size"])
|
167
|
+
|
168
|
+
def _build_progress_bar():
|
169
|
+
widgets = [
|
170
|
+
pb.DataSize(), '/', pb.DataSize(variable='max_value'),
|
171
|
+
pb.Percentage(),
|
172
|
+
' ',
|
173
|
+
pb.RotatingMarker(),
|
174
|
+
' ',
|
175
|
+
pb.ETA(),
|
176
|
+
' ',
|
177
|
+
pb.FileTransferSpeed()
|
178
|
+
]
|
179
|
+
return pb.ProgressBar(
|
180
|
+
max_value=total_bytes,
|
181
|
+
widgets=widgets
|
182
|
+
)
|
183
|
+
logger.info("Downloading session .zip")
|
184
|
+
res2 = central.get(f"/xapi/archive/download/{res1.json()['id']}/zip", timeout=(60, 300))
|
185
|
+
res2.raise_for_status()
|
186
|
+
with (
|
187
|
+
open(zip_path := (dicom_dir / f"{res1.json()['id']}.zip"), "wb") as f,
|
188
|
+
_build_progress_bar() as bar
|
189
|
+
):
|
190
|
+
logger.info(f"Request headers: {res2.request.headers}")
|
191
|
+
logger.info(f"Response headers: {res2.headers}")
|
192
|
+
logger.removeHandler(sout_handler)
|
193
|
+
for chunk in res2.iter_content(chunk_size=(chunk_size := 1024)):
|
194
|
+
if chunk:
|
195
|
+
f.write(chunk)
|
196
|
+
cur_bytes += chunk_size
|
197
|
+
bar.update(cur_bytes)
|
181
198
|
logger.addHandler(sout_handler)
|
182
|
-
logger.info("
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
def download_nordic_zips(session: str,
|
188
|
-
central: Interface,
|
189
|
-
session_experiment: pyxnat.jsonutil.JsonTable,
|
190
|
-
session_dicom_dir: Path) -> list[Path]:
|
191
|
-
dat_dir_list = []
|
192
|
-
project_id = session_experiment["project"]
|
193
|
-
exp_id = session_experiment['ID']
|
194
|
-
|
195
|
-
def __digests_identical(zip_path: Path,
|
196
|
-
cnda_file: pyxnat.core.resources.File):
|
197
|
-
if zip_path.is_file(): # Compare digests of zip on CNDA to see if we need to redownload
|
198
|
-
with zip_path.open("rb") as f:
|
199
|
-
if hashlib.md5(f.read()).hexdigest() == cnda_file.attributes()['digest']: # digests match
|
200
|
-
return True
|
201
|
-
return False
|
202
|
-
|
203
|
-
# check for zip file from NORDIC sessions
|
204
|
-
nordic_volumes = central.select(f"/projects/{project_id}/experiments/{exp_id}/resources/NORDIC_VOLUMES/files").get("")
|
205
|
-
logger.info(f"Found {len(nordic_volumes)} 'NORDIC_VOLUMES' for this session")
|
206
|
-
for nv in nordic_volumes:
|
207
|
-
zip_path = session_dicom_dir / nv._uri.split("/")[-1]
|
208
|
-
if not __digests_identical(zip_path, nv):
|
209
|
-
logger.info(f"Downloading {zip_path.name}...")
|
210
|
-
nv.get(zip_path)
|
211
|
-
unzip_path = zip_path.parent / zip_path.stem
|
212
|
-
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
213
|
-
logger.info(f"Unzipping to {unzip_path}...")
|
214
|
-
zip_ref.extractall(unzip_path)
|
215
|
-
dat_dir_list.append(unzip_path)
|
216
|
-
|
217
|
-
return dat_dir_list
|
199
|
+
logger.info("Download complete!")
|
200
|
+
unzipped_dir = unzipped(zip_path, keep_zip=False)
|
201
|
+
recursive_unzip(unzipped_dir, keep_zip=False) # for NORDIC_VOLUMES already zipped up
|
218
202
|
|
219
203
|
|
220
204
|
def dat_dcm_to_nifti(session: str,
|
221
205
|
dat_directory: Path,
|
222
206
|
xml_file_path: Path,
|
223
207
|
session_dicom_dir: Path,
|
224
|
-
|
208
|
+
session_nifti_dir: Path,
|
225
209
|
skip_short_runs: bool = False):
|
226
|
-
|
210
|
+
"""
|
211
|
+
Pair .dcm/.dat files with dcmdat2niix
|
212
|
+
|
213
|
+
:param session: Session identifier
|
214
|
+
:type session: str
|
215
|
+
:param dat_directory: Directory with .dat files
|
216
|
+
:type dat_directory: pathlib.Path
|
217
|
+
:param xml_file_path: Path to session XML
|
218
|
+
:type xml_file_path: pathlib.Path
|
219
|
+
:param session_dicom_dir: Path to directory containing DICOM folders for each series
|
220
|
+
:type session_dicom_dir: pathlib.Path
|
221
|
+
:param session_nifti_dir: Path to directory containing all .dat files
|
222
|
+
:type session_nifti_dir: pathlib.Path
|
223
|
+
:param skip_short_runs: Flag which denotes we don't want to run dcmdat2niix on runs stopped short
|
224
|
+
:type skip_short_runs: bool
|
225
|
+
"""
|
227
226
|
can_convert = False
|
228
227
|
unconverted_series = set()
|
229
228
|
error_series = set()
|
230
229
|
if shutil.which('dcmdat2niix') is not None:
|
231
230
|
can_convert = True
|
232
|
-
|
233
|
-
logger.info(f"Combined .dcm & .dat files (.nii.gz format) will be stored at: {
|
231
|
+
session_nifti_dir.mkdir(parents=True, exist_ok=True)
|
232
|
+
logger.info(f"Combined .dcm & .dat files (.nii.gz format) will be stored at: {session_nifti_dir}")
|
234
233
|
else:
|
235
234
|
logger.warning("dcmdat2niix not installed or has not been added to the PATH. Cannot convert data files into NIFTI")
|
236
235
|
|
@@ -239,26 +238,22 @@ def dat_dcm_to_nifti(session: str,
|
|
239
238
|
if (p / "DICOM").exists()]
|
240
239
|
downloaded_scans.sort()
|
241
240
|
|
242
|
-
# create a map of downloaded scan IDs to UIDs to later match with the UIDs in the .dat files
|
243
241
|
xml_scans = get_xml_scans(xml_file=xml_file_path)
|
244
242
|
# [:-6] is to ignore the trailing '.0.0.0' at the end of the UID string
|
245
243
|
uid_to_id = {s.get("UID")[:-6]:s.get("ID") for s in xml_scans if s.get("ID") in downloaded_scans}
|
246
244
|
|
247
245
|
# collect all of the .dat files and map them to their UIDs
|
248
|
-
dat_files = list(dat_directory.
|
246
|
+
dat_files = list(dat_directory.rglob("*.dat"))
|
249
247
|
uid_to_dats = {uid: [d for d in dat_files if uid in d.name] for uid in uid_to_id.keys()}
|
250
248
|
|
251
249
|
for uid, dats in uid_to_dats.items():
|
252
250
|
series_id = uid_to_id[uid]
|
253
251
|
series_path = session_dicom_dir / series_id / "DICOM"
|
254
252
|
for dat in dats:
|
255
|
-
# if (series_path / dat.name).is_file():
|
256
|
-
# (series_path / dat.name).unlink()
|
257
253
|
shutil.move(dat.resolve(), series_path.resolve())
|
258
254
|
|
259
|
-
# Either there are no accompanying dats, or they were already in the series directory
|
260
255
|
if len(dats) == 0:
|
261
|
-
dats = list(series_path.glob("*.dat"))
|
256
|
+
dats = list(series_path.glob("*.dat")) # see if dats already in series dir
|
262
257
|
|
263
258
|
dcms = list(series_path.glob("*.dcm"))
|
264
259
|
logger.info(f"length of dats: {len(dats)}")
|
@@ -286,8 +281,8 @@ def dat_dcm_to_nifti(session: str,
|
|
286
281
|
logger.warning("Could not find the mismatched dicom")
|
287
282
|
|
288
283
|
# run the dcmdat2niix subprocess
|
289
|
-
logger.info(f"Running dcmdat2niix on series {series_id}
|
290
|
-
dcmdat2niix_cmd = shlex.split(f"dcmdat2niix -ba
|
284
|
+
logger.info(f"Running dcmdat2niix on series {series_id}")
|
285
|
+
dcmdat2niix_cmd = shlex.split(f"dcmdat2niix -ba y -z o -w 1 -o {session_nifti_dir} {series_path}")
|
291
286
|
with subprocess.Popen(dcmdat2niix_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
|
292
287
|
while p.poll() is None:
|
293
288
|
for line in p.stdout:
|
@@ -332,11 +327,9 @@ def main():
|
|
332
327
|
help="Query by CNDA experiment identifier (default is to query by experiment 'label', which may be ambiguous)",
|
333
328
|
action='store_true')
|
334
329
|
parser.add_argument("-p", "--project_id",
|
335
|
-
help="Specify the project ID to narrow down search. Recommended if the session list is not
|
336
|
-
parser.add_argument("
|
337
|
-
help="
|
338
|
-
parser.add_argument("-n", "--ignore_nordic_volumes",
|
339
|
-
help="Don't download a NORDIC_VOLUMES folder from CNDA if it exists.",
|
330
|
+
help="Specify the project ID to narrow down search. Recommended if the session list is not experiment ids.")
|
331
|
+
parser.add_argument("--skip_dcmdat2niix",
|
332
|
+
help="If NORDIC_VOLUMES folder is available, don't perform dcmdat2niix pairing step",
|
340
333
|
action='store_true')
|
341
334
|
parser.add_argument("--map_dats", type=Path,
|
342
335
|
help="""The path to a directory containting .dat files you wish to pair with DICOM files. Using this argument
|
@@ -344,15 +337,15 @@ def main():
|
|
344
337
|
run dcmdat2niix""")
|
345
338
|
parser.add_argument("--log_dir", type=Path,
|
346
339
|
help="Points to a specified directory that will store the log file. Will not make the directory if it doesn't exist.")
|
347
|
-
parser.add_argument("
|
340
|
+
parser.add_argument("--skip_short_runs",
|
348
341
|
action="store_true",
|
349
342
|
help="Flag to indicate that runs stopped short should not be converted to NIFTI")
|
350
|
-
parser.add_argument("--skip_unusable",
|
351
|
-
help="Don't download any scans marked as 'unusable' in the XML",
|
352
|
-
action='store_true')
|
353
343
|
parser.add_argument("--dats_only",
|
354
344
|
help="Skip downloading DICOMs, only try to pull .dat files",
|
355
345
|
action='store_true')
|
346
|
+
parser.add_argument("--keep_zip",
|
347
|
+
help="Option to keep downloaded .zip file after unzipping",
|
348
|
+
action='store_true')
|
356
349
|
args = parser.parse_args()
|
357
350
|
|
358
351
|
# validate argument inputs
|
@@ -364,9 +357,6 @@ def main():
|
|
364
357
|
args.log_dir.mkdir(parents=True, exist_ok=True)
|
365
358
|
log_path = args.log_dir / f"cnda-dl_{datetime.datetime.now().strftime('%m-%d-%y_%I:%M%p')}.log"
|
366
359
|
|
367
|
-
if args.scan_number and len(args.session_list) > 1:
|
368
|
-
parser.error("'--scan_number' can only be specified when there is only one session/experiment to download")
|
369
|
-
|
370
360
|
if args.map_dats and not args.map_dats.is_dir():
|
371
361
|
parser.error(f"'--map_dats' directory does not exist: {args.map_dats}")
|
372
362
|
|
@@ -376,49 +366,51 @@ def main():
|
|
376
366
|
logger.addHandler(file_handler)
|
377
367
|
logger.addHandler(sout_handler)
|
378
368
|
|
379
|
-
logger.info("Starting cnda-dl
|
369
|
+
logger.info("Starting cnda-dl")
|
380
370
|
logger.info(f"Log will be stored at {log_path}")
|
381
371
|
|
382
372
|
# set up data paths
|
383
373
|
session_list = args.session_list
|
384
|
-
|
374
|
+
dicom_dir = args.dicom_dir
|
385
375
|
if hasattr(args, 'xml_dir') and args.xml_dir is not None:
|
386
376
|
xml_path = args.xml_dir
|
387
377
|
else:
|
388
|
-
xml_path =
|
378
|
+
xml_path = dicom_dir
|
389
379
|
|
390
|
-
if not
|
391
|
-
handle_dir_creation(
|
380
|
+
if not dicom_dir.is_dir():
|
381
|
+
handle_dir_creation(dicom_dir)
|
392
382
|
if not xml_path.is_dir():
|
393
383
|
handle_dir_creation(xml_path)
|
394
384
|
|
395
385
|
# set up CNDA connection
|
396
386
|
central = None
|
397
387
|
if not args.map_dats:
|
398
|
-
central = Interface(server="https://cnda.wustl.edu/")
|
388
|
+
central = px.Interface(server="https://cnda.wustl.edu/")
|
389
|
+
atexit.register(central.disconnect)
|
399
390
|
|
400
391
|
# main loop
|
401
392
|
for session in session_list:
|
393
|
+
download_success = False
|
402
394
|
xml_file_path = xml_path / f"{session}.xml"
|
403
|
-
session_dicom_dir =
|
404
|
-
|
395
|
+
session_dicom_dir = dicom_dir / session
|
396
|
+
session_nifti_dir = dicom_dir / f"{session}_nii"
|
405
397
|
# if only mapping is needed
|
406
398
|
if args.map_dats:
|
407
399
|
# map the .dat files to the correct scans and convert the files to NIFTI
|
408
|
-
nifti_path = dicom_path / f"{session}_nii"
|
409
400
|
try:
|
410
401
|
dat_dcm_to_nifti(session=session,
|
411
402
|
dat_directory=args.map_dats,
|
412
403
|
xml_file_path=xml_file_path,
|
413
404
|
session_dicom_dir=session_dicom_dir,
|
414
|
-
|
405
|
+
session_nifti_dir=session_nifti_dir,
|
415
406
|
skip_short_runs=args.skip_short_runs)
|
416
407
|
except Exception:
|
417
408
|
logger.exception(f"Error moving the .dat files to the appropriate scan directories and converting to NIFTI for session: {session}")
|
409
|
+
download_success = False
|
418
410
|
continue
|
419
411
|
|
420
412
|
# download the experiment data
|
421
|
-
logger.info(f"Starting download of session {session}
|
413
|
+
logger.info(f"Starting download of session {session}")
|
422
414
|
|
423
415
|
# try to retrieve the experiment corresponding to this session
|
424
416
|
exp = None
|
@@ -434,47 +426,38 @@ def main():
|
|
434
426
|
|
435
427
|
except Exception:
|
436
428
|
logger.exception("Error retrieving the experiment from the given parameters. Double check your inputs or enter more specific parameters.")
|
429
|
+
download_success = False
|
437
430
|
continue
|
438
431
|
|
439
|
-
# download the xml for this session
|
440
432
|
download_xml(central=central,
|
441
433
|
subject_id=exp["xnat:mrsessiondata/subject_id"],
|
442
434
|
project_id=exp["project"],
|
443
435
|
file_path=xml_file_path)
|
444
|
-
# try to download the files for this experiment
|
445
436
|
if not args.dats_only:
|
446
437
|
try:
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
except Exception:
|
438
|
+
download_experiment_zip(central=central,
|
439
|
+
exp=exp,
|
440
|
+
dicom_dir=dicom_dir,
|
441
|
+
xml_file_path=xml_file_path,
|
442
|
+
keep_zip=args.keep_zip)
|
443
|
+
except Exception as e:
|
454
444
|
logger.exception(f"Error downloading the experiment data from CNDA for session: {session}")
|
445
|
+
logger.exception(f"{e=}")
|
446
|
+
download_success = False
|
455
447
|
continue
|
456
448
|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
xml_file_path=xml_file_path,
|
470
|
-
session_dicom_dir=session_dicom_dir,
|
471
|
-
nifti_path=nifti_path,
|
472
|
-
skip_short_runs=args.skip_short_runs)
|
473
|
-
except Exception:
|
474
|
-
logger.exception(f"Error downloading 'NORDIC_VOLUMES' and converting to NIFTI for session: {session}")
|
475
|
-
continue
|
476
|
-
|
477
|
-
logger.info("\n...Downloads Complete")
|
449
|
+
nordic_dat_dir = session_dicom_dir / "NORDIC_VOLUMES"
|
450
|
+
recursive_unzip(nordic_dat_dir)
|
451
|
+
if args.skip_dcmdat2niix or not nordic_dat_dir.is_dir():
|
452
|
+
continue
|
453
|
+
dat_dcm_to_nifti(session=session,
|
454
|
+
dat_directory=nordic_dat_dir,
|
455
|
+
xml_file_path=xml_file_path,
|
456
|
+
session_dicom_dir=session_dicom_dir,
|
457
|
+
session_nifti_dir=session_nifti_dir,
|
458
|
+
skip_short_runs=args.skip_short_runs)
|
459
|
+
if download_success:
|
460
|
+
logger.info("\nDownloads Complete")
|
478
461
|
|
479
462
|
|
480
463
|
if __name__ == "__main__":
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
|
4
|
+
|
5
|
+
class Colors:
|
6
|
+
RESET = "\033[0m"
|
7
|
+
BOLD = "\033[1m"
|
8
|
+
UNDERLINE = "\033[4m"
|
9
|
+
|
10
|
+
BLACK = "\033[30m"
|
11
|
+
RED = "\033[31m"
|
12
|
+
GREEN = "\033[32m"
|
13
|
+
YELLOW = "\033[33m"
|
14
|
+
BLUE = "\033[34m"
|
15
|
+
MAGENTA = "\033[35m"
|
16
|
+
CYAN = "\033[36m"
|
17
|
+
WHITE = "\033[37m"
|
18
|
+
DARK_GREY = "\033[90m"
|
19
|
+
LIGHT_RED = "\033[91m"
|
20
|
+
LIGHT_GREEN = "\033[92m"
|
21
|
+
LIGHT_YELLOW = "\033[93m"
|
22
|
+
LIGHT_BLUE = "\033[94m"
|
23
|
+
LIGHT_MAGENTA = "\033[95m"
|
24
|
+
LIGHT_CYAN = "\033[96m"
|
25
|
+
LIGHT_WHITE = "\033[97m"
|
26
|
+
|
27
|
+
# Background colors
|
28
|
+
BACK_BLACK = "\033[40m"
|
29
|
+
BACK_RED = "\033[41m"
|
30
|
+
BACK_GREEN = "\033[42m"
|
31
|
+
BACK_YELLOW = "\033[43m"
|
32
|
+
BACK_BLUE = "\033[44m"
|
33
|
+
BACK_MAGENTA = "\033[45m"
|
34
|
+
BACK_CYAN = "\033[46m"
|
35
|
+
BACK_WHITE = "\033[47m"
|
36
|
+
BACK_DARK_GREY = "\033[100m"
|
37
|
+
BACK_LIGHT_RED = "\033[101m"
|
38
|
+
BACK_LIGHT_GREEN = "\033[102m"
|
39
|
+
BACK_LIGHT_YELLOW = "\033[103m"
|
40
|
+
BACK_LIGHT_BLUE = "\033[104m"
|
41
|
+
BACK_LIGHT_MAGENTA = "\033[105m"
|
42
|
+
BACK_LIGHT_CYAN = "\033[106m"
|
43
|
+
BACK_LIGHT_WHITE = "\033[107m"
|
44
|
+
|
45
|
+
|
46
|
+
class ParensOnRightFormatter1(logging.Formatter):
|
47
|
+
def format(self, record):
|
48
|
+
log_message = f"{record.msg}"
|
49
|
+
log_level = f"{record.levelname}"
|
50
|
+
func_name = (f"{record.funcName}")
|
51
|
+
if func_name[-1] == '.':
|
52
|
+
func_name[-1] = f"{Colors.DARK_GREY}.{Colors.RESET}"
|
53
|
+
# Determine total width of terminal window
|
54
|
+
terminal_width = os.get_terminal_size()[0]
|
55
|
+
# Calculate the right margin position for the log level and function name
|
56
|
+
if log_level == "INFO":
|
57
|
+
right_margin_text = f"{Colors.LIGHT_GREEN}({log_level}, {func_name}){Colors.RESET}"
|
58
|
+
elif log_level == "WARNING":
|
59
|
+
right_margin_text = f"{Colors.YELLOW}({log_level}, {func_name}){Colors.RESET}"
|
60
|
+
elif log_level == "ERROR":
|
61
|
+
right_margin_text = f"{Colors.RED}({log_level}, {func_name}){Colors.RESET}"
|
62
|
+
necessary_padding = terminal_width - len(log_message) - len(right_margin_text)
|
63
|
+
# Ensure padding is non-negative
|
64
|
+
padding = f'{Colors.DARK_GREY}.{Colors.RESET}' * max(0, necessary_padding)
|
65
|
+
return f"{log_message}{padding}{right_margin_text}"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import zipfile
|
2
|
+
from pathlib import Path
|
3
|
+
import logging
|
4
|
+
|
5
|
+
logger = logging.getLogger()
|
6
|
+
|
7
|
+
|
8
|
+
def unzipped(zip_path: str | Path, keep_zip: bool = False):
|
9
|
+
if isinstance(zip_path, str):
|
10
|
+
zip_path = Path(zip_path)
|
11
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
12
|
+
logger.info(f"Unzipping {zip_path}...")
|
13
|
+
zip_ref.extractall(zip_path.parent)
|
14
|
+
if not keep_zip:
|
15
|
+
logger.info(f"Removing {zip_path}...")
|
16
|
+
zip_path.unlink()
|
17
|
+
return zip_path.with_suffix('')
|
18
|
+
|
19
|
+
|
20
|
+
def recursive_unzip(top_dir: str | Path, keep_zip: bool = False):
|
21
|
+
if isinstance(top_dir, str):
|
22
|
+
top_dir = Path(top_dir)
|
23
|
+
zips_exist = True
|
24
|
+
while zips_exist:
|
25
|
+
zips_exist = False
|
26
|
+
for zip_path in top_dir.rglob("*.zip"):
|
27
|
+
zips_exist = True
|
28
|
+
unzipped(zip_path)
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
25
25
|
"pyxnat>=1.6.2",
|
26
26
|
]
|
27
27
|
name = "cnda_dl"
|
28
|
-
version = "1.0
|
28
|
+
version = "1.2.0"
|
29
29
|
description = "A command line utility for downloading fMRI data from CNDA"
|
30
30
|
readme = "README.md"
|
31
31
|
|
@@ -34,5 +34,6 @@ cnda-dl = "cnda_dl.cli:main"
|
|
34
34
|
|
35
35
|
[dependency-groups]
|
36
36
|
dev = [
|
37
|
+
"pudb>=2025.1",
|
37
38
|
"pytest<9.0.0,>=8.2.2",
|
38
39
|
]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|