-
Notifications
You must be signed in to change notification settings - Fork 264
ENH: Add Nifti1DicomExtension + test #296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
0d115dd
Add Nifti1DicomExtension + test
kastman 6bb904d
Add @dicom_test decorator to Pass w/o pydicom
kastman f591ee3
Use BytesIO (py3k compatibility)
kastman 267304c
Fix BytesIO import (again), py3.3 compat
kastman dbe3946
Fix writing typo, add writing tests
kastman b5b6550
Zeropad Extension to 16 bytes, test writing
kastman a2719d3
A little cleanup
kastman 32fd4b1
Read full datasets (with TransferSyntax)
kastman bf75e49
Use write_dataset for pydicom < 0.9.9 compat
kastman 9d0cdee
PEP8 Whitespace
kastman 947034d
Test TransferSyntax, fix zipped reading.
kastman 3f35e5c
Revert "Read Full Datasets Using TransferSyntax"
kastman cf72525
Revert "PEP8 Whitespace"
kastman 58b517c
Revert "Use write_dataset for pydicom < 0.9.9 compat"
kastman 40d24dd
Revert "Read full datasets (with TransferSyntax)"
kastman e690148
PEP8 + consistency cleanup
kastman 336f6bb
Create empty dataset or use one if provided
kastman cc3af8e
Add Doc page on NIfTI header
kastman d897a62
PEP8 and header warning (doc)
kastman 13ae9ff
Import and doc cleanup
kastman f11ccf9
PEP8 Cleanup
kastman 44f7430
NiftiHeader determines dicom byte encoding in extension
kastman 0f897b7
BF: a couple of tiny fixes
matthew-brett e9b6256
Merge master branch into update-plus-for-296
matthew-brett 2065503
RF: move dicom / pydicom imports into own module
matthew-brett 14af19c
STY: remove a couple of blank lines for PEP8
matthew-brett 8534bb4
Merge pull request #1 from matthew-brett/update-plus-for-296
kastman 65de3fe
DOC Add docstring to DicomExtension __init__
kastman 674bb25
RF Type cleanup from MB’s suggestions
kastman 5e37a58
BF Import pydicom.values if first import is successful
kastman baddbde
BF Correct dicom import
kastman 8fb8616
TST Assert TypeError for bad content type
kastman d87ee32
BF Remove unnecessary VR validation
kastman e066203
DOC Correct docstring per numpy guidelines
kastman 71a3ce4
TST Add empty content test case
kastman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
.. _dicom-niftiheader: | ||
|
||
############################## | ||
DICOM Tags in the NIfTI Header | ||
############################## | ||
|
||
NIfTI images include an extended header (see the `NIfTI Extensions Standard`_) | ||
to store, amongst others, DICOM tags and attributes. When NiBabel loads a NIfTI | ||
file containing DICOM information (a NIfTI extension with ``ecode == 2``), it | ||
parses it and returns a pydicom dataset as the content of the NIfTI extension. | ||
This can be read and written to in order to facilitate communication with | ||
software that uses specific DICOM codes found in the NIfTI header. | ||
|
||
For example, the commercial PMOD software stores the Frame Start and Duration | ||
times of images using the DICOM tags (0055, 1001) and (0055, 1004). Here's an | ||
example of an image created in PMOD with those stored times accessed through | ||
nibabel. | ||
|
||
.. code:: python | ||
|
||
>> import nibabel as nib | ||
>> nim = nib.load('pmod_pet.nii') | ||
>> dcmext = nim.header.extensions[0] | ||
>> dcmext | ||
Nifti1Extension('dicom', '(0054, 1001) Units CS: 'Bq/ml' | ||
(0055, 0010) Private Creator LO: 'PMOD_1' | ||
(0055, 1001) [Frame Start Times Vector] FD: [0.0, 30.0, 60.0, ..., 13720.0, 14320.0] | ||
(0055, 1004) [Frame Durations (ms) Vector] FD: [30000.0, 30000.0, 30000.0,600000.0, 600000.0]')) | ||
|
||
+-------------+--------------------------------+---------------------------------------------------------+ | ||
| Tag | Name | Value | | ||
+=============+================================+=========================================================+ | ||
| (0054, 1001)| Units | CS: 'Bq/ml' | | ||
+-------------+--------------------------------+---------------------------------------------------------+ | ||
|(0055, 0010) | Private Creator | LO: 'PMOD_1' | | ||
+-------------+--------------------------------+---------------------------------------------------------+ | ||
|(0055, 1001) | [Frame Start Times Vector] | FD: [0.0, 30.0, 60.0, ..., 13720.0, 14320.0 | | ||
+-------------+--------------------------------+---------------------------------------------------------+ | ||
|(0055, 1004) | [Frame Durations (ms) Vector] | FD: [30000.0, 30000.0, 30000.0, ..., 600000.0, 600000.0 | | ||
+-------------+--------------------------------+---------------------------------------------------------+ | ||
|
||
Access each value as you would with pydicom:: | ||
|
||
>> ds = dcmext.get_content() | ||
>> start_times = ds[0x0055, 0x1001].value | ||
>> durations = ds[0x0055, 0x1004].value | ||
|
||
Creating a PMOD-compatible header is just as easy:: | ||
|
||
>> nim = nib.load('pet.nii') | ||
>> nim.header.extensions | ||
[] | ||
>> from dicom.dataset import Dataset | ||
>> ds = Dataset() | ||
>> ds.add_new((0x0054,0x1001),'CS','Bq/ml') | ||
>> ds.add_new((0x0055,0x0010),'LO','PMOD_1') | ||
>> ds.add_new((0x0055,0x1001),'FD',[0.,30.,60.,13720.,14320.]) | ||
>> ds.add_new((0x0055,0x1004),'FD',[30000.,30000.,30000.,600000.,600000.]) | ||
>> dcmext = nib.nifti1.Nifti1DicomExtension(2,ds) # Use DICOM ecode 2 | ||
>> nim.header.extensions.append(dcmext) | ||
>> nib.save(nim,'pet_withdcm.nii') | ||
|
||
Be careful! Many imaging tools don't maintain information in the extended | ||
header, so it's possible [likely] that this information may be lost during | ||
routine use. You'll have to keep track, and re-write the information if | ||
required. | ||
|
||
Optional Dependency Note: If pydicom is not installed, nibabel uses a generic | ||
:class:`nibabel.nifti1.Nifti1Extension` header instead of parsing DICOM data. | ||
|
||
.. _`NIfTI Extensions Standard`: http://nifti.nimh.nih.gov/nifti-1/documentation/nifti1fields/nifti1fields_pages/extension.html | ||
|
||
.. include:: links_names.txt |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
''' | ||
from __future__ import division, print_function | ||
import warnings | ||
from io import BytesIO | ||
|
||
import numpy as np | ||
import numpy.linalg as npl | ||
|
@@ -24,6 +25,7 @@ | |
from . import analyze # module import | ||
from .spm99analyze import SpmAnalyzeHeader | ||
from .casting import have_binary128 | ||
from .pydicom_compat import have_dicom, pydicom as pdcm | ||
|
||
# nifti1 flat header definition for Analyze-like first 348 bytes | ||
# first number in comments indicates offset in file header in bytes | ||
|
@@ -257,7 +259,7 @@ def __init__(self, code, content): | |
""" | ||
Parameters | ||
---------- | ||
code : int|str | ||
code : int or str | ||
Canonical extension code as defined in the NIfTI standard, given | ||
either as integer or corresponding label | ||
(see :data:`~nibabel.nifti1.extension_codes`) | ||
|
@@ -379,13 +381,101 @@ def write_to(self, fileobj, byteswap): | |
fileobj.write(b'\x00' * (extstart + rawsize - fileobj.tell())) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PEP8 - two lines between classes, two lines between functions one line between methods. |
||
|
||
class Nifti1DicomExtension(Nifti1Extension): | ||
"""NIfTI1 DICOM header extension | ||
|
||
This class is a thin wrapper around pydicom to read a binary DICOM | ||
byte string. If pydicom is available, content is exposed as a Dicom Dataset. | ||
Otherwise, this silently falls back to the standard NiftiExtension class | ||
and content is the raw bytestring loaded directly from the nifti file | ||
header. | ||
""" | ||
def __init__(self, code, content, parent_hdr=None): | ||
""" | ||
Parameters | ||
---------- | ||
code : int or str | ||
Canonical extension code as defined in the NIfTI standard, given | ||
either as integer or corresponding label | ||
(see :data:`~nibabel.nifti1.extension_codes`) | ||
content : bytes or pydicom Dataset or None | ||
Extension content - either a bytestring as read from the NIfTI file | ||
header or an existing pydicom Dataset. If a bystestring, the content | ||
is converted into a Dataset on initialization. If None, a new empty | ||
Dataset is created. | ||
parent_hdr : :class:`~nibabel.nifti1.Nifti1Header`, optional | ||
If a dicom extension belongs to an existing | ||
:class:`~nibabel.nifti1.Nifti1Header`, it may be provided here to | ||
ensure that the DICOM dataset is written with correctly corresponding | ||
endianness; otherwise it is assumed the dataset is little endian. | ||
|
||
Notes | ||
----- | ||
|
||
code should always be 2 for DICOM. | ||
""" | ||
|
||
self._code = code | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docstring here with parameters definitions would be useful. |
||
if parent_hdr: | ||
self._is_little_endian = parent_hdr.endianness == '<' | ||
else: | ||
self._is_little_endian = True | ||
if isinstance(content, pdcm.dataset.Dataset): | ||
self._is_implicit_VR = False | ||
self._raw_content = self._mangle(content) | ||
self._content = content | ||
elif isinstance(content, bytes): # Got a byte string - unmangle it | ||
self._raw_content = content | ||
self._is_implicit_VR = self._guess_implicit_VR() | ||
ds = self._unmangle(content, self._is_implicit_VR, | ||
self._is_little_endian) | ||
self._content = ds | ||
elif content is None: # initialize a new dicom dataset | ||
self._is_implicit_VR = False | ||
self._content = pdcm.dataset.Dataset() | ||
else: | ||
raise TypeError("content must be either a bytestring or a pydicom " | ||
"Dataset. Got %s" % content.__class__) | ||
|
||
def _guess_implicit_VR(self): | ||
"""Try to guess DICOM syntax by checking for valid VRs. | ||
|
||
Without a DICOM Transfer Syntax, it's difficult to tell if Value | ||
Representations (VRs) are included in the DICOM encoding or not. | ||
This reads where the first VR would be and checks it against a list of | ||
valid VRs | ||
""" | ||
potential_vr = self._raw_content[4:6].decode() | ||
if potential_vr in pdcm.values.converters.keys(): | ||
implicit_VR = False | ||
else: | ||
implicit_VR = True | ||
return implicit_VR | ||
|
||
def _unmangle(self, value, is_implicit_VR=False, is_little_endian=True): | ||
bio = BytesIO(value) | ||
ds = pdcm.filereader.read_dataset(bio, | ||
is_implicit_VR, | ||
is_little_endian) | ||
return ds | ||
|
||
def _mangle(self, dataset): | ||
bio = BytesIO() | ||
dio = pdcm.filebase.DicomFileLike(bio) | ||
dio.is_implicit_VR = self._is_implicit_VR | ||
dio.is_little_endian = self._is_little_endian | ||
ds_len = pdcm.filewriter.write_dataset(dio, dataset) | ||
dio.seek(0) | ||
return dio.read(ds_len) | ||
|
||
|
||
# NIfTI header extension type codes (ECODE) | ||
# see nifti1_io.h for a complete list of all known extensions and | ||
# references to their description or contacts of the respective | ||
# initiators | ||
extension_codes = Recoder(( | ||
(0, "ignore", Nifti1Extension), | ||
(2, "dicom", Nifti1Extension), | ||
(2, "dicom", Nifti1DicomExtension if have_dicom else Nifti1Extension), | ||
(4, "afni", Nifti1Extension), | ||
(6, "comment", Nifti1Extension), | ||
(8, "xcede", Nifti1Extension), | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe mention that dicom headers read as generic if pydicom not installed?