ImageProcessor.getPixel(int x,int y) not returning raw pixel values

imagej
plugin

#1

I am having some trouble with a DICOM image stored as signed short 12-bit data in a 16-bit image array. I thought that the ImageProcessor.getPixel(int x,int y) function returned the raw pixel value at position (x,y) and it seems to for most DICOM images however in images from one particular system the values returned have the re-scale intercept from the DICOM tags already applied. Since this is not the case for most DICOM images I am having trouble making my plugin behave consistently across all system.
If I use the import raw option and skip the bytes containing the DICOM header then I can see that the raw pixels in the image are as expected so it is ImageJ that is applying the re-scale intercept.
Please could someone advise if I am mis-understanding how getPixel is supposed to work?


#2

Hello John -

Let me speculate a bit:

As far as I know, the only signed pixel type ImageJ (IJ1, at least)
supports internally is float. My guess is that when you read a signed
12-bit data-type DICOM image into ImageJ, it converts it into one of
the types it actually works with internally. I would guess it would convert
it into float to preserve the signedness.

ImageProcessor is an abstract class with various concrete subclasses,
such as ByteProcessor and FloatProcessor. It is such concrete
subclasses that actually implement getPixel (int, int). (Note,
that int FloatProcessor.getPixel (int, int) returns an
int that simply contains the float’s bits, but for which java needs to
be told to interpret these bits as a float using
float Float.intBitsToFloat (int).)

I would suggest that you probe the exact type of your
ImageProcessor, call it ip, using ip.getClass().getName().
I would expect that different DICOM images get converted to
different types of ImageProcessor's, depending what actual
image format is contained in the DICOM wrapper.

Perhaps when ImageJ opens a signed 12-bit DICOM image, it
(reasonably) performs the rescaling so that the ImageJ-format
ImageProcessor used under the hood “makes sense” for displaying,
processing, etc.

Given that ImageJ doesn’t work (directly) with signed 12-bit data,
could you give us some idea what sort of processing you need to
do for which you need the raw pixels?

Also, if you could provide a link to the DICOM image for which
you are having this issue, we could speculate less about how
ImageJ imports it.

Thanks, mm


#3

Thank you very much for getting back so quickly!
This is my first post and I’ve messed it up already… the pixel data in this case is actually signed. I will edit the question to correct it.

I’m not sure how to upload a file on here to share it. Perhaps some pixel values would help clarify instead?

getPixel result 32757
getPixelValue result -11.0
re-scale intercept -1024

How i would normally convert from getPixel result to a calibrated value would be to subtract (2^16)/2 i.e. 32767 and then apply the re-scale intercept of -1024. This gives -1035 though when actually the correct value is -11.0 as returned by getPixelValue. The method i have been using has worked on many thousand images from many different manufacturers and models. This is the first image that my methodology returns an incorrect value for. Have I just been lucky so far?

I don’t use getPixelValue because in some routines i need to apply a new calibration that differs from that in the DICOM header. In addition the getPixelValue historically did not return the desired result for me from some scanner images but until now the getPixel function always returned something that made sense to me. Perhaps my understanding is flawed though? It is certainly limited!


#4

Hello John -

You say:

To confirm: Your individual pixel values are 16-bit words with the top
four bits guaranteed to be zero. Is this correct? And (before any
rescaling or calibration) their values range from 0 to 4095. Correct?

Try using the “Upload” button in the message-editing window of the
forum web interface. If the forum rejects your .dcm file, try renaming
it (not converting it) to .jpg. Don’t worry if the image doesn’t display
in your browser – we just need the link to download it and view it
locally. If that still doesn’t work, try zipping up your .dcm file into a
.zip file, and uploading that. If that still doesn’t work, find some
file-sharing site, upload your image there, and post the link here.

This is helpful. Could you also post the “raw” pixel value you get when
you “use the import raw option”? And perhaps also post these various
values for a couple of additional example pixels.

First some context: I know little about DICOM, but, as I understand it,
DICOM is not a specific image format, per se, but rather a wrapper
that contains a bunch of health-information-centric metadata, and then
the image itself in one of many different image formats. (And some
metadata field tells you what the actual image format is.)

When you import a DICOM image into ImageJ, the import code
converts the specific DICOM image format into one of ImageJ’s
internal image formats.

I would imagine that “unsigned short 12-bit data in a 16-bit image array”
gets stored in a ShortProcessor. You are correct that (modulo a couple of
semantic quibbles) int ImagePorcessor.getPixel (int, int)
returns the raw pixel value as stored in ImageJ’s format. This may
or may not be the pixel value stored in the image file. (Consider a
.jpg file that doesn’t even have explicitly stored individual pixel values.)

Here is the code for ShortProcessor.getPixel() (from
ShortProcessor.java):

    public int getPixel(int x, int y) {
        if (x>=0 && x<width && y>=0 && y<height)
            return pixels[y*width+x]&0xffff;
        else
            return 0;
    }

Indeed, it returns ImageJ’s raw pixel value.

(Note that ShortProcessor.getPixelValue() translates the
raw pixel value through a floating-point calibration table, if present,
so your example results show that the ImageJ DICOM import is
setting up the calibration table for you, presumably based on
something in the DICOM metadata.)

What looks odd to me is that the value you quote,
“getPixel result 32757”, is not in the range of a 12-bit unsigned
integer. So something is happening between your .dcm file and
ImageJ’s ImageProcessor.

Could you check and let us know the concrete run-time type of the
ImageProcessor you are using? Also show us the “import raw”
value of some example .dcm-file pixels. Describe explicitly how you
import the DICOM image – the exact steps, whether you use code or
menu commands. Lastly – somehow – post a link to your problematic
DICOM image.

It’s possible that ImageJ is doing something sensible with your
particular DICOM image. There could also a bug or unexpected
“feature” in the DICOM import code that is used to process your
specific image type. But it’s hard to give you concrete advice
without an example image to work with and a recipe to reproduce
your DICOM import issue.

Thanks, mm


#5

Hi mountain_man,

thanks again for being so quick to respond.

In fact I believe that some pixels have a value set at 63536 which is a padding value used in some DICOM images to indicate that pixels equal to or exceeding this value are not actually a part of the image. It is usually used when (as is the case here) the image itself is not rectangular or square. There is a DICOM tag indicating padding (0028,0120) Pixel padding value:62536.
This will mean that before calibration there will be values higher than 4095.

I have uploaded the file as a zipped folder.

The corresponding raw pixel value I get is 1013.

I agree with your description of the DICOM standard.

I see… the semantic quibbles are quite important!

I have uploaded the image as a zip.

I have used my own c code to read the file and also tried importing as raw in imagej using the following settings:

Image type: 16-bit signed
Width:512
Height:512
Offset to first image:1066
Number of image:1
Gap between images: 0
Little endian byte order: checked
all others checkboxes unchecked

The runtime image processor I am using is as opened automatically by imageJ when opening the file: I think it’s a ShortProcessor.

Thanks again for all your help.


#6

Very sorry, I thought I had uploaded the file but I have now had a message saying new users cannot upload files. I will try to find another file sharing site.


#7

I have uploaded it to WeTransfer.com. It’s acessible via:

https://we.tl/t-53UJmasyOK


#8

Hello John -

For completeness and convenience, here is your Image00020
DICOM image file – with the incorrect “.tif” file extension added
to trick the forum:

Image00020|nullxnull

Even though this not-really-.tif file won’t display correctly in the
forum, I believe people should be able to download it from the
forum, remove the .tif extension, and open it in ImageJ if they’re
interested.

Here is a (real) “.jpg” of your image, made by opening the DICOM
image in ImageJ / Fiji and saving it as a .jpg:

Just to confirm, does the .jpg image look like what you expect?
(I’m not intending to use the .jpg file to investigate your issue.
I just uploaded it as a cross-check and so the forum viewers
can easily see a version of the image.)

Thanks, mm


#9

Hi again,

glad you could access the image and thanks for posting it on here. Yes, the jpg is a reasonable representation of the image. If you are interested it is a CT scan of a water filled, cylindrical, perspex container used for testing certain aspects of the scanner performance (I thought it best not to send a patient image!).

all the best
John


#10

This is a signed 16-bit image and the intercept of -1024 has been applied, as expected. Here is JavaScript code that reads the value of the pixel at (0,0) four different ways:

  img = IJ.getImage();
  ip = img.getProcessor();
  print("getPixelValue: "+ip.getPixelValue(0,0));
  print("getPixel: "+(ip.getPixel(0,0)));
  print("get: "+ip.get(0,0));
  print("getf: "+ip.getf(0,0));

And here is the output:

  getPixelValue: -3024
  getPixel: 29744
  get: 29744
  getf: 29744

As you can see, getPixelValue() is the only method that correctly reads signed 16-bit values because it is the only one that applies the calibration function (y=-32768+x) that converts 16-bit values from unsigned to signed.


#11

Thank you very much for your help Wayne. I am atraid I am still a bit confused but possibly closer to framing my question in a more concise and clear format…

Does this mean that for DICOM images with pixels stored as signed short the DICOM tag (0028,1052) Rescale Intercept is not applied to the calibrated value? In DICOM images with pixels stored in an unsigned format, imagej does additionally apply this tag to the calibration. I thought that this was the correct behaviour for DICOM files. But in this case, applying the rescale intercept to the getPixel value would result in incorrect values.


#12

Is the re-scale intercept applied before the getPixel result is returned and if so is this behaviour guaranteed for all signed short DICOM files? If so I can easily solve my problem.


#13

Hello John -

I think I understand most of what is going on here.

In short, the “raw” pixels in ImageJ’s short[] pixel array in the
ShortProcessor containing the image data are not the same
as the raw pixels in the DICOM file. As to whether this is right or
wrong or good or bad, I have no opinion.

You said that you routinely process DICOM images without this
issue. It would be interesting if you could upload a (similar) image
that does not have this issue to your WeTransfer site and post the
link here. It might be worth figuring out how this particular image
breaks your usual workflow.

In summary (details below):

First, your DICOM image is a signed 16-bit image.

Second, when a signed 16-bit DICOM image is read in, 2^15 = 32768
is added (or subtracted) from the DICOM file’s pixel values. (I
guess this is so that negative values – which are algebraically
smaller than positive values – test smaller when interpreted as
unsigned values.)

Third, it appears that the DICOM calibration (intercept = -1024)
is also applied to the pixels stored in the ShortProcessor.

Thus:

   ImageJ-pixel-value = DICOM-pixel-value - 31744 (mod 2^16 = 65536)

(where 31744 = 2^15 - 1024)

The ImageJ Calibration (the difference between getPixel()
and getPixelValue() is not the DICOM calibration (of -1024),
but rather -32768.

So, you get the right getPixelValue() values (I suppose), but the
“raw” ShortProcessor getPixel() pixel values don’t match the
raw pixel values in the DICOM file.

Some details:

This can be illustrated as follows:

(I’m using stock Fiji, recently auto-updated, “ImageJ 2.0.0-rc-68/1.52h”.)

Use File > Open... to open your DICOM file. Run
Image > Show Info... to see the DICOM header tags. In
particular, you see the pixels are signed 16-bit integers. Also note
“Pixel Padding Value: 63536”.

While your image is open, run the following jython (python) script:
(File > New > Script...)

from ij import IJ
imp = IJ.getImage()
print 'imp type =', imp.getClass().getName()
print 'title =', imp.getTitle()
ip = imp.getProcessor()
print 'ip type =', ip.getClass().getName()
print 'pixel (0, 0):', ip.getPixel (0, 0), ip.getPixelValue (0, 0)
print 'pixel (95, 95):', ip.getPixel (95, 95), ip.getPixelValue (95, 95)
print 'pixel (255, 255):', ip.getPixel (255, 255), ip.getPixelValue (255, 255)
cal = imp.getCalibration()
print 'cal.isSigned16Bit() =', cal.isSigned16Bit()
print 'cal.getCValue (0) =', cal.getCValue (0)
print 'cal.getCValue (29744) =', cal.getCValue (29744)
print 'cal.getCoefficients() =', cal.getCoefficients()

When I run this script, I get the following output:

imp type = ij.plugin.DICOM
title = Image00020
ip type = ij.process.ShortProcessor
pixel (0, 0): 29744 -3024.0
pixel (95, 95): 32877 109.0
pixel (255, 255): 32776 8.0
cal.isSigned16Bit() = True
cal.getCValue (0) = -32768.0
cal.getCValue (29744) = -3024.0
cal.getCoefficients() = array('d', [-32768.0, 1.0])

Note first that pixel (0, 0) has value 29744. This is the padding
value of 63536 modified as described above. That is,
29744 = 63536 + (32768 - 1024) mod 65536.

The ImagePlus is actually a DICOM (a subclass of ImagePlus).
It is DICOM (and functions it calls) that reads in and parses
the DICOM file.

DICOM.java

Its ImageProcessor is indeed a ShortProcessor.

You can see that the -1024 calibration lives in the ShortProcessor's
pixel array, while the ImagePlus's Calibration object does not
contain the DICOM calibration, but, as Wayne pointed out, serves to
undo the shift of 32768.

(I do not know what would happen with this calibration stuff with an
unsigned DICOM image.)

I do see in the code where the 32768 shift is implemented:

ImageReader.java

            if (fi.intelByteOrder) {
                if (fi.fileType==FileInfo.GRAY16_SIGNED)
                    for (int i=base,j=0; i<(base+pixelsRead); i++,j+=2)
                        pixels[i] = (short)((((buffer[j+1]&0xff)<<8) | (buffer[j]&0xff))+32768);
                else
                    for (int i=base,j=0; i<(base+pixelsRead); i++,j+=2)
                        pixels[i] = (short)(((buffer[j+1]&0xff)<<8) | (buffer[j]&0xff));
            } else {
                if (fi.fileType==FileInfo.GRAY16_SIGNED)
                    for (int i=base,j=0; i<(base+pixelsRead); i++,j+=2)
                        pixels[i] = (short)((((buffer[j]&0xff)<<8) | (buffer[j+1]&0xff))+32768);
                else
                    for (int i=base,j=0; i<(base+pixelsRead); i++,j+=2)
                        pixels[i] = (short)(((buffer[j]&0xff)<<8) | (buffer[j+1]&0xff));
            }

I have not, however, tracked down where the -1024 calibration gets
applied to the “raw” ShortProcessor pixels. I would guess that some
setCalibration() call doesn’t just attach a Calibration object to
the ImagePlus, but actually modifies the pixel values. But I haven’t
found any code where this happens.

Thanks, mm


#14

The Rescale Intercept (-1024 in your image) is added to every pixel when the image is opened. This is done for signed short DICOMs that have a non-zero Rescale Intercept and a Rescale Slope of 1.


#15

This is done in the run() method of DICOM.java (https://imagej.nih.gov/ij/developer/source/ij/plugin/DICOM.java.html):

 } else if (fi.fileType==FileInfo.GRAY16_SIGNED) {
     if (dd.rescaleIntercept!=0.0 && dd.rescaleSlope==1.0)
     ip.add(dd.rescaleIntercept);
 } ...

#16

Thank you very much for all of your help mountain man. I have certainly learnt a lot and I think I can consider my question thoroughly answered!

Yes, I do routinely process DICOM images without encountering this issue. Unfortunately I do not store them for long after processing them so I don’t have any more examples of a signed DICOM image (which I rarely encounter). If I do encounter any more then I will try to remember to upload them.

With an unsigned DICOM image the ImagePlus’s Calibration object does contain the DICOM calibration. I will try to upload an example.

I think this is what was confusing me - the behaviour is not consistent between data types. From what you and Wayne have said this is the expected behaviour and I can adapt my code now that I know that.

Thank you again for all of your help
John


#17

Unsigned document (remove the .tif ending to open in imageJ).

UnsignedDICOM|nullxnull


#18

Thank you very much for all your help and clarification Wayne. Now that I know the expected behaviour I can edit my code to make it handle these images.

all the best
John


#19

Hello Wayne -

Yes, thanks for directing me to that bit of code. This is indeed the
piece of logic that I was looking for.

Thanks, mm


#20

Hello John -

Thanks for posting this follow-up example.

I see (from Image > Show Info...) that this is an unsigned 12-bit
image.

Here is the result of running on it the script I posted earlier:

imp type = ij.plugin.DICOM
title = UnsignedDICOM
ip type = ij.process.ShortProcessor
pixel (0, 0): 0 -1024.0
pixel (95, 95): 99 -925.0
pixel (255, 255): 858 -166.0
cal.isSigned16Bit() = False
cal.getCValue (0) = -1024.0
cal.getCValue (29744) = 28720.0
cal.getCoefficients() = array('d', [-1024.0, 1.0])

We see that the getPixel() pixel values are equal to the pixel
values in the DICOM file, the DICOM calibration is not applied to the
ShortProcessor pixels, but rather shows up in the ImageProcessor's
Calibration object. So, indeed, being unsigned turns off the
modification of the “raw” pixel values.

I made a signed version of your UnsignedDICOM DICOM file (called
UnsignedDICOM_signed). Here it is (with a fake “.tif” file extension
to trick the forum):

UnsignedDICOM_signed|nullxnull

Here is the result of running my script on it:

imp type = ij.plugin.DICOM
title = UnsignedDICOM_signed
ip type = ij.process.ShortProcessor
pixel (0, 0): 31744 -1024.0
pixel (95, 95): 31843 -925.0
pixel (255, 255): 32602 -166.0
cal.isSigned16Bit() = True
cal.getCValue (0) = -32768.0
cal.getCValue (29744) = -3024.0
cal.getCoefficients() = array('d', [-32768.0, 1.0])

As you can see, ImageJ uses a 16-bit ShortProcessor to store the
signed 12-bit DICOM image. The pixel munging happens (as with your
original Image00020 image), and the Calibration object is not the
DICOM calibration, but rather is used to undo the 32768 shift applied
to the pixels.

It turns out that the only negative pixel values in your original
Image00020 image are the “padding” pixels with value (as an
unsigned short) of 63536. So, for completeness, I modified this
file to make it unsigned. (This changes the padding from dark
to light, but doesn’t otherwise affect the image.) Here it is, named
Image00020_unsigned, with the fake “.tif” extension:

Image00020_unsigned|nullxnull

Here is the result of running the script:

imp type = ij.plugin.DICOM
title = Image00020_unsigned
ip type = ij.process.ShortProcessor
pixel (0, 0): 63536 62512.0
pixel (95, 95): 1133 109.0
pixel (255, 255): 1032 8.0
cal.isSigned16Bit() = False
cal.getCValue (0) = -1024.0
cal.getCValue (29744) = 28720.0
cal.getCoefficients() = array('d', [-1024.0, 1.0])

As (by now) expected, we get the unmunged pixels, and the
Calibration object is the DICOM calibration.

So this all looks quite consistent with Wayne’s explanation.

For your workflow it would appear that you can probe the
ImagePlus's Calibration object with

Calibration.isSigned16Bit()

If false, use your original workflow. If true use a modified workflow
that accounts for the munged pixels. Alternatively, you could look at
Calibration.getCoefficients(). If they match your DICOM
calibration, use your old workflow, and if they instead show the
-32768 shift, use the new.

Thanks, mm