PageRenderTime 55ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/xnat/util.py

https://github.com/VUIIS/pyxnat_notes
Python | 391 lines | 329 code | 12 blank | 50 comment | 15 complexity | f6b59bf79a99187e35fd15cfedb82d41 MD5 | raw file
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """This module is meant to provide more-useful interface functions to an XNAT
  4. server than currently provided by pyxnat."""
  5. import os
  6. import time
  7. from ConfigParser import ConfigParser
  8. import subprocess as sb
  9. try:
  10. import nibabel as nib
  11. use_nibabel = True
  12. except ImportError:
  13. use_nibabel = False
  14. from pyxnat import Interface
  15. from pyxnat.core.resources import Project, Subject, Experiment, Scan
  16. from pyxnat.core.errors import DatabaseError
  17. ALLOWED_KEYS = {Subject: set(['group', 'src', 'pi_lastname',
  18. 'pi_firstname', 'dob', 'yob', 'age', 'gender',
  19. 'handedness', 'ses', 'education', 'educationDesc',
  20. 'race', 'ethnicity', 'weight', 'height',
  21. 'gestational_age', 'post_menstrual_age',
  22. 'birth_weight']),
  23. Project: set(['ID', 'secondary_ID', 'name',
  24. 'description', 'keywords', 'alias', 'pi_lastname',
  25. 'pi_firstname', 'note']),
  26. Experiment: set(['visit_id', 'date', 'ID', 'project',
  27. 'label', 'time', 'note', 'pi_firstname', 'pi_lastname',
  28. 'validation_method', 'validation_status',
  29. 'validation_date', 'validation_notes', 'subject_ID',
  30. 'subject_label', 'subject_project', 'scanner',
  31. 'operator', 'dcmAccessionNumber', 'dcmPatientId',
  32. 'dcmPatientName', 'session_type', 'modality',
  33. 'UID', 'coil', 'fieldStrength', 'marker',
  34. 'stabilization', 'studyType', 'patientID',
  35. 'patientName', 'stabilization', 'scan_start_time',
  36. 'injection_start_time', 'tracer_name',
  37. 'tracer_startTime', 'tracer_dose', 'tracer_sa',
  38. 'tracer_totalmass', 'tracer_intermediate',
  39. 'tracer_isotope', 'tracer_transmissions',
  40. 'tracer_transmissions_start']),
  41. Scan: set(['ID', 'type', 'UID', 'note', 'quality',
  42. 'condition', 'series_description', 'documentation',
  43. 'scanner', 'modality', 'frames', 'validation_method',
  44. 'validation_status', 'validation_date',
  45. 'validation_notes', 'coil', 'fieldStrength', 'marker',
  46. 'stabilization', 'orientation', 'scanTime',
  47. 'originalFileName', 'fileType', 'transaxialFOV',
  48. 'acqType', 'facility', 'numPlanes', 'numFrames',
  49. 'numGates', 'planeSeparation', 'binSize', 'dataType'])}
  50. NIBABEL_TO_XNAT = {
  51. 'session_error': (lambda x: 'Good' if x == 0 else 'Error', 'validation_status'),
  52. 'dim': (lambda x: str(x[4]), 'frames'),
  53. 'datatype': (lambda x: str(x.dtype), 'dataType'),
  54. 'descrip': (lambda x: ' '.join(str(x).split()), 'series_description')
  55. }
  56. def xnat(cfg=os.path.join(os.path.expanduser('~'), '.xnat.cfg'), server_url=None,
  57. username=None, password=None, cache=os.path.join(os.path.expanduser('~'), '.xnat.cache')):
  58. """Initialize and test xnat connection from a previously-stored cfg file
  59. or passed credentials
  60. Parameters
  61. ----------
  62. cfg: str
  63. Path to a stored interface configuration
  64. This is not from Interface.save_config but rather looks like this:
  65. [xnat]
  66. user: [your user name]
  67. password: [your password]
  68. server: [your xnat server]
  69. cache: [dir]
  70. server_url: str
  71. url to your xnat install
  72. username: str
  73. user name
  74. password: str
  75. user password
  76. cache: str
  77. Cache directory
  78. Returns
  79. -------
  80. A valid Interface object to your XNAT system.
  81. This may throw an error from pyxnat.core.errors
  82. """
  83. if not (cfg or (server_url and username and upass)):
  84. raise ValueError("Must pass cfg file or server/username/password")
  85. if (server_url and user and upass):
  86. server = server_url
  87. user = username
  88. pword = password
  89. else:
  90. cp = ConfigParser()
  91. with open(cfg) as f:
  92. cp.readfp(f)
  93. user = cp.get('xnat', 'user')
  94. pword = cp.get('xnat', 'password')
  95. server = cp.get('xnat', 'server')
  96. cachedir = cp.get('xnat', 'cache')
  97. if '~' in cachedir:
  98. cachedir = os.path.expanduser(cachedir)
  99. xnat = Interface(server=server,
  100. user=user,
  101. password=pword,
  102. cachedir=cachedir)
  103. # Because the constructor doesn't test the connection, make sure 'admin' is
  104. # in the list of users. Any errors are passed to the caller.
  105. if user not in xnat.manage.users():
  106. raise ValueError('This XNAT is weird.')
  107. xnat._memtimeout = 0.001
  108. return xnat
  109. def project(xnat, name, proj_data={}):
  110. """ Create a new project/update project info
  111. Parameters
  112. ----------
  113. xnat: pyxnat.Interface object
  114. The connection to your xnat system
  115. name: str
  116. Project name
  117. proj_data: dict
  118. Project data you'd like to initialize
  119. Returns
  120. -------
  121. pjt: Project object
  122. """
  123. pjt = _check_parent_and_get(xnat, xnat.select.project, name)
  124. if proj_data:
  125. succeeded, bad = _update_metadata(pjt, proj_data)
  126. if not succeeded:
  127. raise ValueError("Bad project keys: %s" % ' '.join(bad))
  128. return pjt
  129. def subject(project, name, sub_data={}):
  130. """ Create a new subject/Update subject info
  131. WARNING: Previously stored data will be overwritten
  132. Parameters
  133. ----------
  134. project: pyxnat.Interface.project()
  135. An established project
  136. name: str
  137. Subject identifier
  138. data: dict
  139. Demographic data to set in xnat
  140. See 'xnat:subjectData' at
  141. http://docs.xnat.org/XNAT+REST+XML+Path+Shortcuts
  142. for allowed keys
  143. Returns
  144. -------
  145. sub: a valid subject object
  146. """
  147. sub = _check_parent_and_get(project, project.subject, name)
  148. if sub_data:
  149. succeeded, bad = _update_metadata(sub, sub_data)
  150. if not succeeded:
  151. raise ValueError("Bad subject data keys: %s" % ' '.join(bad))
  152. return sub
  153. def experiment(subject, name, exp_data={}):
  154. """ Create/Update a subject's experiment
  155. Name must be unique!
  156. Parameters
  157. ----------
  158. subject: Subject object
  159. The subject to which you want to create/update the experiment
  160. name: str
  161. Experiment string name
  162. exp_data: dict
  163. Experiment metadata to set in xnat
  164. See 'xnat:experimentData' at
  165. http://docs.xnat.org/XNAT+REST+XML+Path+Shortcuts
  166. for allowed keys
  167. Returns
  168. -------
  169. exp: a valid experiment object
  170. """
  171. exp = _check_parent_and_get(subject, subject.experiment, name)
  172. if exp_data:
  173. succeeded, bad = _update_metadata(exp, exp_data)
  174. if not succeeded:
  175. raise ValueError("Bad experiment data keys: %s" % ' '.join(bad))
  176. return exp
  177. def scan(experiment, name, scan_data={}):
  178. """ Create/Update an experiment's scan
  179. Parameters
  180. ----------
  181. experiment: experiment object
  182. The experiment for which you want to create/update a scan
  183. name: str
  184. Scan string name
  185. scan_data: dict
  186. Scan metadata to set in xnat
  187. See 'xnat:imageScanData' at
  188. http://docs.xnat.org/XNAT+REST+XML+Path+Shortcuts
  189. for allowed keys
  190. Returns
  191. -------
  192. scan: a valid scan object
  193. """
  194. scan = _check_parent_and_get(experiment, experiment.scan, name)
  195. if scan_data:
  196. succeeded, bad = _update_metadata(scan, scan_data)
  197. if not succeeded:
  198. raise ValueError("Bad scan data keys: %s" % ' '.join(bad))
  199. return scan
  200. def resource(scan, name):
  201. """ Create/Update a scan's resource
  202. Parameters
  203. ----------
  204. scan: scan object
  205. The scan for which you want to create/update a resource
  206. name: str
  207. Resource name
  208. Returns
  209. -------
  210. res: a valid resource object
  211. """
  212. res = _check_parent_and_get(scan, scan.resource, name)
  213. # Not sure what to specify as far as resource metadata?
  214. return res
  215. def add_nifti(scan, res_name, fpath, file_name='image.nii', other_md={}):
  216. """ Upload a nifti into a scan
  217. Parameters
  218. ----------
  219. scan: scan object
  220. res_name: str
  221. Name of the resource this file will be a child of
  222. file_name: str
  223. The name of the file in the string
  224. fpath: str
  225. Path on local machine of file to upload
  226. other_md: dict
  227. Other appropriate metadata
  228. """
  229. res = resource(scan, res_name)
  230. md = {}
  231. if use_nibabel:
  232. try:
  233. img = nib.load(fpath)
  234. hdr = img.get_header()
  235. except IOError:
  236. raise IOError("%s doesn't exist on the filesystem." % fpath)
  237. except nib.spatialimages.ImageFileError:
  238. raise ValueError("%s doesn't appear to be a proper nifti1 image"
  239. % fpath)
  240. # map nib header keys to xnat metadata
  241. for k, t in NIBABEL_TO_XNAT.items():
  242. f = t[0]
  243. md[t[1]] = f(hdr[k])
  244. # hard-code some variables
  245. md['validation_method'] = 'nibabel header check/pyxnat tools'
  246. md['validation_date'] = time.strftime('%Y-%m-%d %H:%M:%S')
  247. md['note'] = 'uploaded with pyxnat tools'
  248. # update md with passed arg
  249. md.update(other_md)
  250. # send scan metadata
  251. s, bk = _update_metadata(scan, md)
  252. # Upload
  253. res.file(file_name).put(fpath)
  254. def _update_metadata(xnat_obj, new_data={}):
  255. """ Update metadata for a xnat object
  256. Parameters
  257. ----------
  258. xnat_obj: any xnat object
  259. The object whose metadata you wish to update
  260. new_data: dict
  261. Data whose keys must match allowed keys
  262. Returns
  263. -------
  264. succeeded: bool
  265. That update worked
  266. bad_keys: seq
  267. If succeeded, empty, otherwise a sequence of the keys not accepted by
  268. xnat
  269. """
  270. if not xnat_obj.exists():
  271. raise ValueError("This object doesn't exist in xnat")
  272. succeeded, bad_keys = _key_check(type(xnat_obj), new_data.keys())
  273. if succeeded:
  274. # do the mset
  275. xnat_obj.attrs.mset(new_data)
  276. # TODO: check that the mset worked
  277. # for key, val in new_data.items():
  278. # good_update = xnat_obj.attrs.get(key) == val
  279. # if not good_update:
  280. # # TODO change to warning
  281. # print("WARNING: %s wasn't updated" % key)
  282. return succeeded, bad_keys
  283. def _check_parent_and_get(parent, creator_fn, name):
  284. """ Private method to check resource owner validity and create/return
  285. the new child
  286. Parameters
  287. ----------
  288. parent: Some xnat object
  289. The owning object, i.e. the object directly owned the desired child
  290. creator_fn: fn
  291. function handle used to create new child
  292. name: str
  293. label for new child
  294. Returns
  295. -------
  296. child: valid xnat object guaranteed to exist in the xnat system
  297. """
  298. if hasattr(parent, 'exists'):
  299. if not parent.exists():
  300. raise ValueError("Parent %s doesn't exist" % parent)
  301. child = creator_fn(name)
  302. if not child.exists():
  303. child.create()
  304. # This might fail, actually
  305. if not child.exists():
  306. msg = "Cannot create object (probably a privilege issue)"
  307. raise DatabaseError(msg)
  308. return child
  309. def _key_check(check_type, keys):
  310. """ Private method to validate parameters before resource creation.
  311. Parameters
  312. ----------
  313. check_type: type
  314. resource type (call with something like (type(obj) )
  315. keys: iterable
  316. parameters to check
  317. Returns
  318. -------
  319. passed: bool
  320. True if keys match xnat parameters, False if not
  321. bad_keys: iterable
  322. keys the caller specified that xnat won't accept
  323. """
  324. if check_type not in ALLOWED_KEYS:
  325. raise NotImplementedError("Cannot currently check %s" % check_type)
  326. key_set = set(keys)
  327. passed = False
  328. bad_keys = []
  329. if ALLOWED_KEYS[check_type].issuperset(key_set):
  330. passed = True
  331. else:
  332. bad_keys.extend(key_set.difference(ALLOWED_KEYS[check_type]))
  333. return passed, bad_keys
  334. def dcm_to_nii(dcm, out_dir):
  335. """ Use dcm2nii to convert dcm files to nifti format """
  336. call = 'dcm2nii -e n -d n -g n -f y -n y -p n -v y -o %(out)s %(dcm)s' % {'out':out_dir, 'dcm':dcm}
  337. try:
  338. output = sb.check_output(call.split())
  339. except sb.CalledProcessError:
  340. output = "DCM --> NII conversion failure\n"
  341. return output