Skip to content
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

[MRG+1] Fix handling of bit-packed PixelData #627

Merged
merged 6 commits into from
Apr 24, 2018

Conversation

fedorov
Copy link
Contributor

@fedorov fedorov commented Apr 20, 2018

Reference Issue

Aims to resolve the problem identified in #293 (review)

What does this implement/fix? Explain your changes.

  • bits were unpacked from the left, which was incorrect, see discussion in
    [MRG + 1] pixel_array for images with self.BitsAllocate=1 #293 (review).
    Correct unpack order implemented now, whereas first pixel from the left in
    the image frame corresponds to the first bit from the right in the packed
    PixelData
  • fixed consistency checks verifying the size of the PixelData
  • reordered the code to perform unpacking after consistency checks
  • removed the dependency on numpy.unpackbits and the associated exception

Any other comments?

The functionality of the code was verified using the DICOM Segmentation image
dataset being developed in this PR (3x23x38, which illustrates a case where
bits of the individual frame are not aligned at the byte boundary, and the total
length of pixel bits is less than the number of bits required to encode the bytes
in PixelData): QIICR/dcmqi#334,
and this issue: QIICR/dcmqi#341. In turn, consistency of
the rendering of that dataset with the implementation was confirmed for independent
implementations (Brainlab and DCMTK dcm2pnm).

We might consider existing tests, since they are not sufficient to identify the bug in the original implementation of the feature (wrong bit pack order) and incorrect calculation of the expected pixel length for datasets where frame is not byte-aligned.

@pep8speaks
Copy link

pep8speaks commented Apr 20, 2018

Hello @fedorov! Thanks for updating the PR.

Cheers ! There are no PEP8 issues in this Pull Request. 🍻

Comment last updated on April 24, 2018 at 16:50 Hours UTC

@fedorov fedorov force-pushed the fix-seg-pixeldata branch 3 times, most recently from 21e5933 to 49edbf7 Compare April 20, 2018 22:34
@fedorov fedorov changed the title [MRG] BUG: fix handling of bit-packed PixelData [MRG] Fix handling of bit-packed PixelData Apr 20, 2018
This commit aims to fix the functionality originally introduced in pydicom#293:

* bits were unpacked from the left, which was incorrect, see discussion in
pydicom#293 (review).
Correct unpack order implemented now, whereas first pixel from the left in
the image frame corresponds to the first bit from the right in the first
byte of the packed PixelData
* fixed consistency checks verifying the size of the PixelData
* reordered the code to perform unpacking after consistency checks
* removed the dependency on numpy.unpackbits and the associated exception

The functionality of the code was verified using the DICOM Segmentation image
dataset being developed in this PR (3x23x38, which illustrates a case where
bits of the individual frame are not aligned at the byte boundary, and the total
length of pixel bits is less than the number of bits required to encode the bytes
in PixelData): QIICR/dcmqi#334,
and this issue: QIICR/dcmqi#341. In turn, consistency of
the rendering of that dataset with the implementation was confirmed for independent
implementations (Brainlab and DCMTK dcm2pnm).
for bit in range(bit, bit+8):
pixel_array[bit] = byte & 1
byte >>= 1
bit += 1
Copy link
Member

Choose a reason for hiding this comment

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

This fails in Python 2 because byte is a str. You may need to convert it to int using ord or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

@mrbean-bremen
Copy link
Member

About the test - the current test is quite sloppy as it only checks the dimensions and a pixel in the middle and a pixel at the edge to be correct. It would make sense to check the whole pixel data, especially if we can replace the test image with a smaller one.

@mrbean-bremen
Copy link
Member

And thanks for the fix!

@fedorov
Copy link
Contributor Author

fedorov commented Apr 21, 2018

@mrbean-bremen thanks for the review! Let me know if you want the updated tests for this PR, or make an issue and address that separately.

@mrbean-bremen
Copy link
Member

Let me know if you want the updated tests for this PR, or make an issue and address that separately.

If would be nice if you could adapt the test, as you are the one who knows the code best. If you don't have the time - a separate issue would also be fine with me, if @darcymason can go with it.

# * DICOM Annex D (examples of encoding)
for byte in pixel_bytearray:
byte = ord(byte)
for bit in range(bit, bit+8):
Copy link
Member

Choose a reason for hiding this comment

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

Now it fails with Python 3. You may use if in_py2: for that line (or make separate loops should that impact the performance). Also, in the same line a tab character has crept in...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry about the tab sloppyness, I used a different text editor.

About the error, it's not the ord() issue, it appears that elements of pixel_bytearray have different type between python 2 and 3! This should be fixed now.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this is because a bytearray in Python 3 is really an array of bytes (where each element has int type), whereas in Python 2 a str is used as bytearray (there is no other), and an element of a str is a str.

Type of pixel_bytearray elements changes between python 2 and 3!
@fedorov
Copy link
Contributor Author

fedorov commented Apr 22, 2018

If would be nice if you could adapt the test, as you are the one who knows the code best. If you don't have the time - a separate issue would also be fine with me, if @darcymason can go with it.

I can definitely work on this, but I would prefer to create an issue and address separately, since I don't want to delay fix of the issue. But if you prefer to address in this PR, I will do it, just will take a bit more time.

@mrbean-bremen
Copy link
Member

That's ok for me - thanks!

@mrbean-bremen mrbean-bremen changed the title [MRG] Fix handling of bit-packed PixelData [MRG+1] Fix handling of bit-packed PixelData Apr 22, 2018
# See the following for details:
# * DICOM 3.5 Sect 8.1.1 (explanation of bit ordering)
# * DICOM Annex D (examples of encoding)
print("Type: "+str(type(pixel_bytearray[0])))
Copy link
Member

Choose a reason for hiding this comment

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

Remove the print statement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

byte = ord(byte)
for bit in range(bit, bit+8):
pixel_array[bit] = byte & 1
byte >>= 1
Copy link
Member

@mrbean-bremen mrbean-bremen Apr 24, 2018

Choose a reason for hiding this comment

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

On a second thought - it might not be a good idea to use isinstance() in the loop for performance reasons. Better use if in_py2.

Copy link
Member

@scaramallion scaramallion Apr 24, 2018

Choose a reason for hiding this comment

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

Or you could do pixel_bytearray[n:n+1] instead. It should return a length 1 str/bytes in python 2/3, then you can just do the ord(byte) conversion without needing a type check. I suppose it depends on how expensive the in_py2 check is.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about thinking along the lines of:

numpy.flip(
    numpy.unpackbits(pixel_bytearray).reshape((length_of_pixel_array, 8)), 
    axis=1).reshape(length_of_pixel_array * 8)

I am pretty sure something like this should work....

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rhaxton can you make a PR with your proposed change to my branch https://github.com/qiicr/pydicom/tree/fix-seg-pixeldata? To me personally, the code I wrote is more readable, and I am not sure if the suggested change leads to any performance improvements (which also I am not sure are critical in this situation). I am not an expert in numpy, so I would defer to someone else doing the numpy-specific optimizations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, I take it back - I oppose this suggestion of changing to use unpackbits, since it means we would only be able to unpack in python 3. Not clear at all what is the advantage of restricting this operation to python 3 only.

Copy link
Contributor Author

@fedorov fedorov Apr 24, 2018

Choose a reason for hiding this comment

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

Better use if in_py2

Done

numpy.frombuffer(pixel_bytearray, dtype='uint8'))
except NotImplementedError:
# PyPy2 does not implement numpy.unpackbits
raise NotImplementedError(
Copy link
Member

Choose a reason for hiding this comment

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

Could you fix up the failing unit test, too?

Copy link
Member

Choose a reason for hiding this comment

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

It can just be removed, likewise the skip condition on OneBitAllocatedTests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (if I understood correctly how things were expected to work ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, interesting - don't understand why CI of the commit prior to removing those tests succeeded.

@scaramallion scaramallion merged commit a1dca34 into pydicom:master Apr 24, 2018
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.

None yet

5 participants