|
12 | 12 | ''' |
13 | 13 | from __future__ import division, print_function |
14 | 14 | import warnings |
| 15 | +from io import BytesIO |
15 | 16 |
|
16 | 17 | import numpy as np |
17 | 18 | import numpy.linalg as npl |
|
24 | 25 | from . import analyze # module import |
25 | 26 | from .spm99analyze import SpmAnalyzeHeader |
26 | 27 | from .casting import have_binary128 |
| 28 | +from .pydicom_compat import have_dicom, pydicom as pdcm |
27 | 29 |
|
28 | 30 | # nifti1 flat header definition for Analyze-like first 348 bytes |
29 | 31 | # first number in comments indicates offset in file header in bytes |
@@ -257,7 +259,7 @@ def __init__(self, code, content): |
257 | 259 | """ |
258 | 260 | Parameters |
259 | 261 | ---------- |
260 | | - code : int|str |
| 262 | + code : int or str |
261 | 263 | Canonical extension code as defined in the NIfTI standard, given |
262 | 264 | either as integer or corresponding label |
263 | 265 | (see :data:`~nibabel.nifti1.extension_codes`) |
@@ -379,13 +381,101 @@ def write_to(self, fileobj, byteswap): |
379 | 381 | fileobj.write(b'\x00' * (extstart + rawsize - fileobj.tell())) |
380 | 382 |
|
381 | 383 |
|
| 384 | +class Nifti1DicomExtension(Nifti1Extension): |
| 385 | + """NIfTI1 DICOM header extension |
| 386 | +
|
| 387 | + This class is a thin wrapper around pydicom to read a binary DICOM |
| 388 | + byte string. If pydicom is available, content is exposed as a Dicom Dataset. |
| 389 | + Otherwise, this silently falls back to the standard NiftiExtension class |
| 390 | + and content is the raw bytestring loaded directly from the nifti file |
| 391 | + header. |
| 392 | + """ |
| 393 | + def __init__(self, code, content, parent_hdr=None): |
| 394 | + """ |
| 395 | + Parameters |
| 396 | + ---------- |
| 397 | + code : int or str |
| 398 | + Canonical extension code as defined in the NIfTI standard, given |
| 399 | + either as integer or corresponding label |
| 400 | + (see :data:`~nibabel.nifti1.extension_codes`) |
| 401 | + content : bytes or pydicom Dataset or None |
| 402 | + Extension content - either a bytestring as read from the NIfTI file |
| 403 | + header or an existing pydicom Dataset. If a bystestring, the content |
| 404 | + is converted into a Dataset on initialization. If None, a new empty |
| 405 | + Dataset is created. |
| 406 | + parent_hdr : :class:`~nibabel.nifti1.Nifti1Header`, optional |
| 407 | + If a dicom extension belongs to an existing |
| 408 | + :class:`~nibabel.nifti1.Nifti1Header`, it may be provided here to |
| 409 | + ensure that the DICOM dataset is written with correctly corresponding |
| 410 | + endianness; otherwise it is assumed the dataset is little endian. |
| 411 | +
|
| 412 | + Notes |
| 413 | + ----- |
| 414 | +
|
| 415 | + code should always be 2 for DICOM. |
| 416 | + """ |
| 417 | + |
| 418 | + self._code = code |
| 419 | + if parent_hdr: |
| 420 | + self._is_little_endian = parent_hdr.endianness == '<' |
| 421 | + else: |
| 422 | + self._is_little_endian = True |
| 423 | + if isinstance(content, pdcm.dataset.Dataset): |
| 424 | + self._is_implicit_VR = False |
| 425 | + self._raw_content = self._mangle(content) |
| 426 | + self._content = content |
| 427 | + elif isinstance(content, bytes): # Got a byte string - unmangle it |
| 428 | + self._raw_content = content |
| 429 | + self._is_implicit_VR = self._guess_implicit_VR() |
| 430 | + ds = self._unmangle(content, self._is_implicit_VR, |
| 431 | + self._is_little_endian) |
| 432 | + self._content = ds |
| 433 | + elif content is None: # initialize a new dicom dataset |
| 434 | + self._is_implicit_VR = False |
| 435 | + self._content = pdcm.dataset.Dataset() |
| 436 | + else: |
| 437 | + raise TypeError("content must be either a bytestring or a pydicom " |
| 438 | + "Dataset. Got %s" % content.__class__) |
| 439 | + |
| 440 | + def _guess_implicit_VR(self): |
| 441 | + """Try to guess DICOM syntax by checking for valid VRs. |
| 442 | +
|
| 443 | + Without a DICOM Transfer Syntax, it's difficult to tell if Value |
| 444 | + Representations (VRs) are included in the DICOM encoding or not. |
| 445 | + This reads where the first VR would be and checks it against a list of |
| 446 | + valid VRs |
| 447 | + """ |
| 448 | + potential_vr = self._raw_content[4:6].decode() |
| 449 | + if potential_vr in pdcm.values.converters.keys(): |
| 450 | + implicit_VR = False |
| 451 | + else: |
| 452 | + implicit_VR = True |
| 453 | + return implicit_VR |
| 454 | + |
| 455 | + def _unmangle(self, value, is_implicit_VR=False, is_little_endian=True): |
| 456 | + bio = BytesIO(value) |
| 457 | + ds = pdcm.filereader.read_dataset(bio, |
| 458 | + is_implicit_VR, |
| 459 | + is_little_endian) |
| 460 | + return ds |
| 461 | + |
| 462 | + def _mangle(self, dataset): |
| 463 | + bio = BytesIO() |
| 464 | + dio = pdcm.filebase.DicomFileLike(bio) |
| 465 | + dio.is_implicit_VR = self._is_implicit_VR |
| 466 | + dio.is_little_endian = self._is_little_endian |
| 467 | + ds_len = pdcm.filewriter.write_dataset(dio, dataset) |
| 468 | + dio.seek(0) |
| 469 | + return dio.read(ds_len) |
| 470 | + |
| 471 | + |
382 | 472 | # NIfTI header extension type codes (ECODE) |
383 | 473 | # see nifti1_io.h for a complete list of all known extensions and |
384 | 474 | # references to their description or contacts of the respective |
385 | 475 | # initiators |
386 | 476 | extension_codes = Recoder(( |
387 | 477 | (0, "ignore", Nifti1Extension), |
388 | | - (2, "dicom", Nifti1Extension), |
| 478 | + (2, "dicom", Nifti1DicomExtension if have_dicom else Nifti1Extension), |
389 | 479 | (4, "afni", Nifti1Extension), |
390 | 480 | (6, "comment", Nifti1Extension), |
391 | 481 | (8, "xcede", Nifti1Extension), |
|
0 commit comments