Skip to content

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 35 commits into from
Mar 24, 2016

Conversation

kastman
Copy link
Contributor

@kastman kastman commented Feb 12, 2015

Hi all,

Here's a thin wrapper class to read DICOM binary header information as encoded with the DICOM extended header code 2, instead of just providing the ugly & useless byte string. The extension code already written was quite helpful; thanks!

This depends on pydicom, but should fall back to a standard Nifti1Extension in the case of an ImportError. This is my first commit to nipy, so I'm not sure how you feel about provisional dependencies like this, but I think delegating the work to pydicom is the right approach.

Few things still to be worked out before you merge:

  • What do I have to do for CI to get this to either load or not load pydicom during testing?
  • This is hard-coded for little endian encodings right now - does anyone know a heuristic? Maybe @darcymason ?
  • This is written inside the Nifti1 format file, since the Nifti2 format seems to just delegate to Nifti1. Is that the right spot for it?
  • I'm not a great test-writer - could I improve on what's there?
  • I added import struct in the test to create DICOM byte-strings, but it looked out of place. Is there a better spot / method?
  • Is there a requirement that Nifti extensions should be divisible by 16 bytes? I saw that in another extension test, but am not aware of the requirement and couldn't find it in the docs. If so, I'll have to add some byte-padding code.

Hope this is helpful; let me know if there are any concerns about it! Cheers,
Erik

@kastman kastman changed the title Add Nifti1DicomExtension + test ENH: Add Nifti1DicomExtension + test Feb 12, 2015
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.05%) to 94.27% when pulling 0d115dd on kastman:ENH-NiftiDicomExt into 96d474c on nipy:master.

@matthew-brett
Copy link
Member

Thanks a lot for doing this.

Absolutely fine for the pydicom optional dependency - we already have that. Have a look at ./nibabel/nicom/tests/test_utils.py for an example of how to deal with the optional dependency.

Did you see: http://nipy.org/nibabel/devel/add_test_data.html for adding test data?

For the tests - the trick is to go through each function / method and ask yourself 'how would I know if this method was doing the wrong thing'. It is a rather tiring process, but it also proves very helpful in learning, at least in my experience.

@kastman
Copy link
Contributor Author

kastman commented Feb 13, 2015

I did see the page for adding test data, but I wasn't sure if it was appropriate to add another sample image (even a small one) for a test like this. Thanks for the tip on test_utils; that will work. Looks like I'm also failing on py3; I'll fix that as well.

@matthew-brett
Copy link
Member

For a test image - if it is smaller than 50 compressed - sure - go ahead and add it to the main repo. Otherwise, a submodule would better. No problem for a very small submodule with only a few images.

@kastman
Copy link
Contributor Author

kastman commented Feb 13, 2015

Ironically, my actual use-case is a PET volume that's 1.6GB, so I'll definitely be making a new image for the test. See what I can do!

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.06%) to 94.26% when pulling 267304c on kastman:ENH-NiftiDicomExt into 96d474c on nipy:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.01%) to 94.31% when pulling dbe3946 on kastman:ENH-NiftiDicomExt into 96d474c on nipy:master.

Also, fix _guess_implicit_VR method.
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.0%) to 94.32% when pulling b5b6550 on kastman:ENH-NiftiDicomExt into 96d474c on nipy:master.

* Remove redundant get size method (inherited)
* Remove unnecessary super()
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.07%) to 94.26% when pulling 32fd4b1 on kastman:ENH-NiftiDicomExt into 96d474c on nipy:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.06%) to 94.26% when pulling bf75e49 on kastman:ENH-NiftiDicomExt into 96d474c on nipy:master.

@matthew-brett
Copy link
Member

Thanks for keeping up the work on this.

Are there any good docs on what the DICOM extension format should contain. I see this : http://nifti.nimh.nih.gov/nifti-1/documentation/nifti1fields/nifti1fields_pages/extension.html - which only says:

2 = DICOM format (i.e., attribute tags and values)

Do you know of anyone else writing these extensions?

is_implicit_VR = False
is_little_endian = False
elif transfer_syntax == dicom.UID.DeflatedExplicitVRLittleEndian:
zipped = fileobj.read()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you got pyflakes or similar running in your editor? Pyflakes tells me that fileobj and zlib are not defined - is this piece of code tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - I took this directly from pydicom, although it wasn't abstracted in a method I could call from nibabel. The only transfer syntax tested here is ImplicitVRLittleEndian, not the zipped or big endian ones. I'll add tests and fix this.

I haven't been using Pyflakes, but just grabbed a bundle to enable it. Thanks for the suggestion!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pyflakes is huge - it really helps picking up this kind of thing.

@kastman
Copy link
Contributor Author

kastman commented Feb 16, 2015

I've only ever needed this once, for PMOD, a closed-source tool to model PET time activity curves. They write dicom tags to indicate the start time and duration of PET frames in a 4d nifti; the closet I could find for documentation is a listing of what they support for nifti. I only noticed they were using the DICOM extension when I saw they were able to read the timing from PMOD-created files and started looking around.

The tags are written with an explicit VR syntax without any metadata. An example header looks like this:

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: [30.0, 30.0, 30.0, ...,600.0, 600.0]')

Everyone else I know that wants DICOM info in NiFtI files uses @moloney 's dcmstack xml encoding; and I see there's already effort for that w/ nibabel (e.g. #232, #290). However, I figured that since I was implementing this, I might as well put in a way of storing full DICOM datasets in case someone wanted to use it.

@matthew-brett
Copy link
Member

Well - I guess we can define the standard format. So would that be the explicit VR syntax? Maybe with little-endian byte order? Do the PMOD files always have little-endian order? And then try our best to read DICOM extensions that are written with implicit VR and big-endian.

@@ -380,6 +380,113 @@ def write_to(self, fileobj, byteswap):
# next 16 byte border
fileobj.write(b'\x00' * (extstart + rawsize - fileobj.tell()))

Copy link
Member

Choose a reason for hiding this comment

The 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.

@kastman
Copy link
Contributor Author

kastman commented Feb 16, 2015

That sounds like a plan; little-endian seems to be preferred from everything I've seen (I'm not even sure how to guess endianness without some magic or prior, which meta-less tags don't have). And I think that being explicit about the VR is preferable to implicit - the intended use-case is archival, not transmission over the network, so clarity has the premium over compression or compactness.

I'm not sure about the endianness of PMOD files - everything I've seen has been little, but there's no doc or guarantee. I could write to the developers if you think it's worth it?

Should we toss reading full metadata dicom datasets until someone wants to use them? Seems like we should honor it if it's present...

@matthew-brett
Copy link
Member

Thinking more - I wonder if we should default to writing as the byte order of the header (header.endianness). Of course this will almost invariably by little-endian these days but still.

I guess we could write explicit VR, header endianness always, and read any endianness, with or without full metadata. I'm neutral about allowing full metadata if you have already implemented it, just because we haven't got an example of anyone using it, and it's more code maintenance - the YAGNI principle.

@kastman
Copy link
Contributor Author

kastman commented Feb 16, 2015

There's two parts to this, the way that the header is actually written and what the header says. The dicom standard says the header is always written ExplicitVR-LittleEndian, but it can list a different method (TransferSyntax) to use for the rest of the file. However, in the case where there is no header (e.g. naked tags like PMOD) there's no TransferSyntax and no way to know the correct encoding; we can only guess it.

How about this? If there is metadata and a TransferSyntax attribute is present, write using whatever the transfer syntax says. If no TransferSyntax, always use little endian. If the dataset was inferred to be Implicit, then write with ImplicitVR, otherwise write with ExplicitVR. That way you're returning as close to what you got as possible.

I agree with you concerning YAGNI, but if I were going to use it, for archiving tags, I would use the whole dataset with metadata, and I would expect the writing to follow the Syntax - not doing so would definitely be a surprise. Plus, I've created example dicoms (~360Bytes) for testing different syntaxes directly from Nibabel's tests, so that reduces maintenance cost a little.

@matthew-brett
Copy link
Member

Sorry - when I was talking about the 'header' I meant the nifti header.

Do we care about what the input transfer syntax is? I mean, endian, or implicit VR? If we specified nifti-header-endian and explicit VR was the standard, could we persuade people that was the right way to save this stuff out?

@kastman
Copy link
Contributor Author

kastman commented Mar 18, 2016

Take a look at this one, @matthew-brett , and see if makes sense. Sorry again for the delay!

Nifti1DicomExtension subclasses Nifti1Extension, but defines its own __init__ and doesn't super() to Nifti1Extensions __init__ in order to allow dicom datasets to be passed directly (see the if __class__ == 'Dataset' logic at nifti1.py#L397). Because of this, I didn't need to alter the header of Nfiti1Extension to accept a parent header. This is simpler and makes fewer changes, but I wonder if I should add it to Nifti1Extension in order to make things clearer in the future?

Also, it looks like the environment is failing. Do we need to rebase now for the CI to build?

@matthew-brett
Copy link
Member

I just did a merge of the master branch (makes it simpler for you to merge into your branch).

I also did a refactor we need to do anyway to clean up importing dicom / pydicom in various places.

I made a pull request into this branch (I hope): kastman#1

Please do check what I did, comments welcome.

self._is_little_endian = parent_hdr.endianness == '<'
else:
self._is_little_endian = True
if content.__class__ == Dataset:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about if isinstance(content, Dataset): ?

@matthew-brett
Copy link
Member

Converting everything to the same endianness of the header sounds good to me.

@matthew-brett
Copy link
Member

Also fine to override the __init__ of Nifti1Extension, as long as you are doing the same stuff in the init, and I think you are.

matthew-brett and others added 10 commits March 18, 2016 16:54
Use externals version of OrderedDict for parrec.

Use ``next(something)`` instead of ``something.__next__``.
* tiny-fixes: (333 commits)
  BF: a couple of tiny fixes
  MAINT: add comment specifying behaviour of shape=None to _hdr_key_dict
  TST: update parrec volume_labels tests to check the specific key order
  TST: expand dualTR parrec test to check warning
  FIX: fix bug in OrderedDict call within parrec get_volume_labels
  TST: add a test using a dummy dual TR .PAR file
  MAINT: change sort_info to an OrderedDict to enforce a consistent ordering for a given .PAR file
  ENH: support multiple TR values in PARREC headers
  TST: add test for parrec2nii CSV output
  MAINT: change parrec2nii volume label output from JSON to CSV
  MAINT: simplify get_volume_labels by removing per-slice attributes
  STY: rename get_dimension_labels to get_volume_labels
  DOC: fix typo in parrec2nii docstring
  TST: add get_dimension_label tests to the 5D and 6D data sets used for testing strict_sort
  STY: trim dynamic_keys via list comprehension
  TST: test_header_dimension_labels() added
  STY: clarify/streamline code based on feedback
  FIX: bugfix to replace np.unique() with _unique_rows() for 2D inputs needed for sorting vector properties.  proper 2D ndarray to list of lists for JSON export in parrec2nii
  TST: add sorted_labels property to FakeHeader in test_parrec.py
  ENH: add sorted_labels function to PARRECHeader and corresponding info to PARRECArrayProxy
  ...

Conflicts:
	Changelog
	nibabel/tests/test_nifti1.py
Move logic for conditionally importing dicom or pydicom into own module,
and use this module where we are using dicom routines.
MRG: merged current master, refactor dicom import
@kastman
Copy link
Contributor Author

kastman commented Mar 19, 2016

Not sure the docstring is super-helpful - take a look? Also, fixed the value import for pydicom <1.

"""
Parameters
----------
code : int|str
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detail point, but I think these should be int or str : see https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt#id5

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I was just following the docstring convention from Nifti1Extension (nifti1.py#L262). I'm fixing that here too. Thanks for the detailed numpy docstring link; it was helpful (I know it's linked at the nibabel developer help as well, but hadn't gone back to that yet).

@kastman
Copy link
Contributor Author

kastman commented Mar 23, 2016

@matthew-brett Anything left that I can tweak or help you with on this one?

@matthew-brett
Copy link
Member

Sorry to be a bit slow - looks good - thanks a lot for your persistence.

@matthew-brett matthew-brett merged commit e1aea51 into nipy:master Mar 24, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants