cnda_dl 1.1.0__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.1.0 → cnda_dl-1.2.0}/PKG-INFO +1 -1
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/cnda_dl/cli.py +161 -231
- cnda_dl-1.2.0/cnda_dl/zip_utils.py +28 -0
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/pyproject.toml +1 -1
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/LICENSE +0 -0
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/README.md +0 -0
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/cnda_dl/.ruff_cache/.gitignore +0 -0
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/cnda_dl/.ruff_cache/0.9.9/15962950311086395899 +0 -0
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/cnda_dl/.ruff_cache/CACHEDIR.TAG +0 -0
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/cnda_dl/__init__.py +0 -0
- {cnda_dl-1.1.0 → cnda_dl-1.2.0}/cnda_dl/formatters.py +0 -0
@@ -1,32 +1,28 @@
|
|
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
|
-
from .formatters import ParensOnRightFormatter1, Colors
|
10
7
|
from glob import glob
|
11
|
-
from matplotlib.ticker import EngFormatter
|
12
8
|
from pathlib import Path
|
13
|
-
|
14
|
-
import
|
15
|
-
import pyxnat
|
9
|
+
import atexit
|
10
|
+
import re
|
16
11
|
import argparse
|
17
12
|
import logging
|
18
13
|
import os
|
19
|
-
import progressbar
|
20
14
|
import shlex
|
21
15
|
import shutil
|
22
|
-
import hashlib
|
23
16
|
import subprocess
|
24
17
|
import sys
|
25
18
|
import xml.etree.ElementTree as et
|
26
|
-
import zipfile
|
27
19
|
import datetime
|
28
20
|
|
29
|
-
|
21
|
+
import pyxnat as px
|
22
|
+
import progressbar as pb
|
23
|
+
|
24
|
+
from .formatters import ParensOnRightFormatter1
|
25
|
+
from .zip_utils import recursive_unzip, unzipped
|
30
26
|
|
31
27
|
default_log_format = "%(levelname)s:%(funcName)s: %(message)s"
|
32
28
|
sout_handler = logging.StreamHandler(stream=sys.stdout)
|
@@ -65,7 +61,7 @@ def handle_dir_creation(dir_path: Path):
|
|
65
61
|
logger.info("Invalid response")
|
66
62
|
|
67
63
|
|
68
|
-
def download_xml(central: Interface,
|
64
|
+
def download_xml(central: px.Interface,
|
69
65
|
subject_id: str,
|
70
66
|
project_id: str,
|
71
67
|
file_path: Path):
|
@@ -77,10 +73,10 @@ def download_xml(central: Interface,
|
|
77
73
|
return True
|
78
74
|
|
79
75
|
|
80
|
-
def retrieve_experiment(central: Interface,
|
76
|
+
def retrieve_experiment(central: px.Interface,
|
81
77
|
session: str,
|
82
78
|
experiment_id: bool = False,
|
83
|
-
project_id: str = None) ->
|
79
|
+
project_id: str = None) -> px.jsonutil.JsonTable:
|
84
80
|
|
85
81
|
query_params = {}
|
86
82
|
if project_id:
|
@@ -93,171 +89,147 @@ def retrieve_experiment(central: Interface,
|
|
93
89
|
return central.array.mrsessions(**query_params)
|
94
90
|
|
95
91
|
|
96
|
-
def get_xml_scans(xml_file: Path
|
97
|
-
|
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
|
98
95
|
|
96
|
+
:param xml_file: path to quality XML
|
97
|
+
:type xml_file: pathlib.Path
|
98
|
+
"""
|
99
99
|
xml_tree = et.parse(xml_file)
|
100
100
|
prefix = "{" + str(xml_tree.getroot()).split("{")[-1].split("}")[0] + "}"
|
101
101
|
scan_xml_entries = xml_tree.getroot().find(
|
102
102
|
f"./{prefix}experiments/{prefix}experiment/{prefix}scans"
|
103
103
|
)
|
104
|
-
if quality_pair:
|
105
|
-
return {s.get("ID"): s.find(f"{prefix}quality").text
|
106
|
-
for s in scan_xml_entries}
|
107
104
|
return scan_xml_entries
|
108
105
|
|
109
106
|
|
110
|
-
def
|
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
|
-
file_attrs = {}
|
167
|
-
series_path = session_dicom_dir / scan / "DICOM"
|
168
|
-
assert series_path.is_dir()
|
169
|
-
file_attrs = {
|
170
|
-
"name": series_path / f._uri.split("/")[-1],
|
171
|
-
"size": fmt(int(f.size())) if f.size() else fmt(0),
|
172
|
-
"isempty": True if not f.size() else False,
|
173
|
-
"isdownloaded": False
|
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"]
|
174
163
|
}
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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()
|
182
186
|
with (
|
183
|
-
|
184
|
-
|
187
|
+
open(zip_path := (dicom_dir / f"{res1.json()['id']}.zip"), "wb") as f,
|
188
|
+
_build_progress_bar() as bar
|
185
189
|
):
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
for
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
bar.update(
|
194
|
-
if file_attrs['isempty']:
|
195
|
-
zero_size_files.append(file_attrs)
|
196
|
-
if file_attrs['isdownloaded']:
|
197
|
-
msg = f"Downloaded file {file_attrs['name']}, {file_attrs['size']} ({cur_file_count} out of {total_file_count})"
|
198
|
-
if len(msg) > (tsize := os.get_terminal_size()[0]) - tsize // 5:
|
199
|
-
msg = msg[:tsize // 2 - 4] + f"{Colors.DARK_GREY}.......{Colors.RESET}" + msg[tsize // 2 + tsize // 5:]
|
200
|
-
# if len(msg) > (tsize := os.get_terminal_size()[0]) and tsize % 2 == 1:
|
201
|
-
# msg = msg[:tsize // 2 - 4] + f"{Colors.DARK_GREY}.......{Colors.RESET}" + msg[tsize // 2 + tsize // 5:]
|
202
|
-
logger.info(msg)
|
203
|
-
# logger.info(f"\tDownloaded file {file_attrs['name']}, {file_attrs['size']} ({cur_file_count} out of {total_file_count})")
|
204
|
-
print(msg)
|
205
|
-
except Exception as exc:
|
206
|
-
print(f"Task ended with an exception {exc}")
|
207
|
-
|
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)
|
208
198
|
logger.addHandler(sout_handler)
|
209
|
-
logger.info("
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
def download_nordic_zips(session: str,
|
215
|
-
central: Interface,
|
216
|
-
session_experiment: pyxnat.jsonutil.JsonTable,
|
217
|
-
session_dicom_dir: Path) -> list[Path]:
|
218
|
-
dat_dir_list = []
|
219
|
-
project_id = session_experiment["project"]
|
220
|
-
exp_id = session_experiment['ID']
|
221
|
-
|
222
|
-
def _digests_identical(zip_path: Path,
|
223
|
-
cnda_file: pyxnat.core.resources.File):
|
224
|
-
if zip_path.is_file(): # Compare digests of zip on CNDA to see if we need to redownload
|
225
|
-
with zip_path.open("rb") as f:
|
226
|
-
if hashlib.md5(f.read()).hexdigest() == cnda_file.attributes()['digest']: # digests match
|
227
|
-
return True
|
228
|
-
return False
|
229
|
-
|
230
|
-
# check for zip file from NORDIC sessions
|
231
|
-
nordic_volumes = central.select(f"/projects/{project_id}/experiments/{exp_id}/resources/NORDIC_VOLUMES/files").get("")
|
232
|
-
logger.info(f"Found {len(nordic_volumes)} 'NORDIC_VOLUMES' for this session")
|
233
|
-
for nv in nordic_volumes:
|
234
|
-
zip_path = session_dicom_dir / nv._uri.split("/")[-1]
|
235
|
-
if not _digests_identical(zip_path, nv):
|
236
|
-
logger.info(f"Downloading {zip_path.name}")
|
237
|
-
nv.get(zip_path)
|
238
|
-
unzip_path = zip_path.parent / zip_path.stem
|
239
|
-
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
240
|
-
logger.info(f"Unzipping to {unzip_path}")
|
241
|
-
zip_ref.extractall(unzip_path)
|
242
|
-
dat_dir_list.append(unzip_path)
|
243
|
-
|
244
|
-
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
|
245
202
|
|
246
203
|
|
247
204
|
def dat_dcm_to_nifti(session: str,
|
248
205
|
dat_directory: Path,
|
249
206
|
xml_file_path: Path,
|
250
207
|
session_dicom_dir: Path,
|
251
|
-
|
208
|
+
session_nifti_dir: Path,
|
252
209
|
skip_short_runs: bool = False):
|
253
|
-
|
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
|
+
"""
|
254
226
|
can_convert = False
|
255
227
|
unconverted_series = set()
|
256
228
|
error_series = set()
|
257
229
|
if shutil.which('dcmdat2niix') is not None:
|
258
230
|
can_convert = True
|
259
|
-
|
260
|
-
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}")
|
261
233
|
else:
|
262
234
|
logger.warning("dcmdat2niix not installed or has not been added to the PATH. Cannot convert data files into NIFTI")
|
263
235
|
|
@@ -266,26 +238,22 @@ def dat_dcm_to_nifti(session: str,
|
|
266
238
|
if (p / "DICOM").exists()]
|
267
239
|
downloaded_scans.sort()
|
268
240
|
|
269
|
-
# create a map of downloaded scan IDs to UIDs to later match with the UIDs in the .dat files
|
270
241
|
xml_scans = get_xml_scans(xml_file=xml_file_path)
|
271
242
|
# [:-6] is to ignore the trailing '.0.0.0' at the end of the UID string
|
272
243
|
uid_to_id = {s.get("UID")[:-6]:s.get("ID") for s in xml_scans if s.get("ID") in downloaded_scans}
|
273
244
|
|
274
245
|
# collect all of the .dat files and map them to their UIDs
|
275
|
-
dat_files = list(dat_directory.
|
246
|
+
dat_files = list(dat_directory.rglob("*.dat"))
|
276
247
|
uid_to_dats = {uid: [d for d in dat_files if uid in d.name] for uid in uid_to_id.keys()}
|
277
248
|
|
278
249
|
for uid, dats in uid_to_dats.items():
|
279
250
|
series_id = uid_to_id[uid]
|
280
251
|
series_path = session_dicom_dir / series_id / "DICOM"
|
281
252
|
for dat in dats:
|
282
|
-
# if (series_path / dat.name).is_file():
|
283
|
-
# (series_path / dat.name).unlink()
|
284
253
|
shutil.move(dat.resolve(), series_path.resolve())
|
285
254
|
|
286
|
-
# Either there are no accompanying dats, or they were already in the series directory
|
287
255
|
if len(dats) == 0:
|
288
|
-
dats = list(series_path.glob("*.dat"))
|
256
|
+
dats = list(series_path.glob("*.dat")) # see if dats already in series dir
|
289
257
|
|
290
258
|
dcms = list(series_path.glob("*.dcm"))
|
291
259
|
logger.info(f"length of dats: {len(dats)}")
|
@@ -314,7 +282,7 @@ def dat_dcm_to_nifti(session: str,
|
|
314
282
|
|
315
283
|
# run the dcmdat2niix subprocess
|
316
284
|
logger.info(f"Running dcmdat2niix on series {series_id}")
|
317
|
-
dcmdat2niix_cmd = shlex.split(f"dcmdat2niix -ba y -z o -w 1 -o {
|
285
|
+
dcmdat2niix_cmd = shlex.split(f"dcmdat2niix -ba y -z o -w 1 -o {session_nifti_dir} {series_path}")
|
318
286
|
with subprocess.Popen(dcmdat2niix_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
|
319
287
|
while p.poll() is None:
|
320
288
|
for line in p.stdout:
|
@@ -359,11 +327,9 @@ def main():
|
|
359
327
|
help="Query by CNDA experiment identifier (default is to query by experiment 'label', which may be ambiguous)",
|
360
328
|
action='store_true')
|
361
329
|
parser.add_argument("-p", "--project_id",
|
362
|
-
help="Specify the project ID to narrow down search. Recommended if the session list is not
|
363
|
-
parser.add_argument("
|
364
|
-
help="
|
365
|
-
parser.add_argument("-n", "--ignore_nordic_volumes",
|
366
|
-
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",
|
367
333
|
action='store_true')
|
368
334
|
parser.add_argument("--map_dats", type=Path,
|
369
335
|
help="""The path to a directory containting .dat files you wish to pair with DICOM files. Using this argument
|
@@ -371,15 +337,15 @@ def main():
|
|
371
337
|
run dcmdat2niix""")
|
372
338
|
parser.add_argument("--log_dir", type=Path,
|
373
339
|
help="Points to a specified directory that will store the log file. Will not make the directory if it doesn't exist.")
|
374
|
-
parser.add_argument("
|
340
|
+
parser.add_argument("--skip_short_runs",
|
375
341
|
action="store_true",
|
376
342
|
help="Flag to indicate that runs stopped short should not be converted to NIFTI")
|
377
|
-
parser.add_argument("--skip_unusable",
|
378
|
-
help="Don't download any scans marked as 'unusable' in the XML",
|
379
|
-
action='store_true')
|
380
343
|
parser.add_argument("--dats_only",
|
381
344
|
help="Skip downloading DICOMs, only try to pull .dat files",
|
382
345
|
action='store_true')
|
346
|
+
parser.add_argument("--keep_zip",
|
347
|
+
help="Option to keep downloaded .zip file after unzipping",
|
348
|
+
action='store_true')
|
383
349
|
args = parser.parse_args()
|
384
350
|
|
385
351
|
# validate argument inputs
|
@@ -391,9 +357,6 @@ def main():
|
|
391
357
|
args.log_dir.mkdir(parents=True, exist_ok=True)
|
392
358
|
log_path = args.log_dir / f"cnda-dl_{datetime.datetime.now().strftime('%m-%d-%y_%I:%M%p')}.log"
|
393
359
|
|
394
|
-
if args.scan_number and len(args.session_list) > 1:
|
395
|
-
parser.error("'--scan_number' can only be specified when there is only one session/experiment to download")
|
396
|
-
|
397
360
|
if args.map_dats and not args.map_dats.is_dir():
|
398
361
|
parser.error(f"'--map_dats' directory does not exist: {args.map_dats}")
|
399
362
|
|
@@ -408,38 +371,38 @@ def main():
|
|
408
371
|
|
409
372
|
# set up data paths
|
410
373
|
session_list = args.session_list
|
411
|
-
|
374
|
+
dicom_dir = args.dicom_dir
|
412
375
|
if hasattr(args, 'xml_dir') and args.xml_dir is not None:
|
413
376
|
xml_path = args.xml_dir
|
414
377
|
else:
|
415
|
-
xml_path =
|
378
|
+
xml_path = dicom_dir
|
416
379
|
|
417
|
-
if not
|
418
|
-
handle_dir_creation(
|
380
|
+
if not dicom_dir.is_dir():
|
381
|
+
handle_dir_creation(dicom_dir)
|
419
382
|
if not xml_path.is_dir():
|
420
383
|
handle_dir_creation(xml_path)
|
421
384
|
|
422
385
|
# set up CNDA connection
|
423
386
|
central = None
|
424
387
|
if not args.map_dats:
|
425
|
-
central = Interface(server="https://cnda.wustl.edu/")
|
388
|
+
central = px.Interface(server="https://cnda.wustl.edu/")
|
389
|
+
atexit.register(central.disconnect)
|
426
390
|
|
427
391
|
# main loop
|
428
392
|
for session in session_list:
|
429
393
|
download_success = False
|
430
394
|
xml_file_path = xml_path / f"{session}.xml"
|
431
|
-
session_dicom_dir =
|
432
|
-
|
395
|
+
session_dicom_dir = dicom_dir / session
|
396
|
+
session_nifti_dir = dicom_dir / f"{session}_nii"
|
433
397
|
# if only mapping is needed
|
434
398
|
if args.map_dats:
|
435
399
|
# map the .dat files to the correct scans and convert the files to NIFTI
|
436
|
-
nifti_path = dicom_path / f"{session}_nii"
|
437
400
|
try:
|
438
401
|
dat_dcm_to_nifti(session=session,
|
439
402
|
dat_directory=args.map_dats,
|
440
403
|
xml_file_path=xml_file_path,
|
441
404
|
session_dicom_dir=session_dicom_dir,
|
442
|
-
|
405
|
+
session_nifti_dir=session_nifti_dir,
|
443
406
|
skip_short_runs=args.skip_short_runs)
|
444
407
|
except Exception:
|
445
408
|
logger.exception(f"Error moving the .dat files to the appropriate scan directories and converting to NIFTI for session: {session}")
|
@@ -466,66 +429,33 @@ def main():
|
|
466
429
|
download_success = False
|
467
430
|
continue
|
468
431
|
|
469
|
-
# download the experiment data
|
470
|
-
logger.info(f"Starting download of session {session}")
|
471
|
-
|
472
|
-
# try to retrieve the experiment corresponding to this session
|
473
|
-
exp = None
|
474
|
-
try:
|
475
|
-
exp = retrieve_experiment(central=central,
|
476
|
-
session=session,
|
477
|
-
experiment_id=args.experiment_id,
|
478
|
-
project_id=args.project_id)
|
479
|
-
if len(exp) == 0:
|
480
|
-
raise RuntimeError("ERROR: CNDA query returned JsonTable object of length 0, meaning there were no results found with the given search parameters.")
|
481
|
-
elif len(exp) > 1:
|
482
|
-
raise RuntimeError("ERROR: CNDA query returned JsonTable object of length >1, meaning there were multiple results returned with the given search parameters.")
|
483
|
-
|
484
|
-
except Exception:
|
485
|
-
|
486
|
-
continue
|
487
|
-
|
488
|
-
# download the xml for this session
|
489
432
|
download_xml(central=central,
|
490
433
|
subject_id=exp["xnat:mrsessiondata/subject_id"],
|
491
434
|
project_id=exp["project"],
|
492
435
|
file_path=xml_file_path)
|
493
|
-
# try to download the files for this experiment
|
494
436
|
if not args.dats_only:
|
495
437
|
try:
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
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:
|
503
444
|
logger.exception(f"Error downloading the experiment data from CNDA for session: {session}")
|
445
|
+
logger.exception(f"{e=}")
|
504
446
|
download_success = False
|
505
447
|
continue
|
506
448
|
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
# try to download NORDIC related files and convert raw data to NIFTI
|
511
|
-
try:
|
512
|
-
nordic_dat_dirs = download_nordic_zips(session=session,
|
513
|
-
central=central,
|
514
|
-
session_experiment=exp,
|
515
|
-
session_dicom_dir=session_dicom_dir)
|
516
|
-
nifti_path = dicom_path / f"{session}_nii"
|
517
|
-
for nordic_dat_path in nordic_dat_dirs:
|
518
|
-
dat_dcm_to_nifti(session=session,
|
519
|
-
dat_directory=nordic_dat_path,
|
520
|
-
xml_file_path=xml_file_path,
|
521
|
-
session_dicom_dir=session_dicom_dir,
|
522
|
-
nifti_path=nifti_path,
|
523
|
-
skip_short_runs=args.skip_short_runs)
|
524
|
-
except Exception:
|
525
|
-
logger.exception(f"Error downloading 'NORDIC_VOLUMES' and converting to NIFTI for session: {session}")
|
526
|
-
download_success = False
|
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():
|
527
452
|
continue
|
528
|
-
|
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)
|
529
459
|
if download_success:
|
530
460
|
logger.info("\nDownloads Complete")
|
531
461
|
|
@@ -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)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|