Tuesday, March 17, 2009

Listing DICOM Header information with dcm4che 2

Hi All,

Some readers have asked me questions regarding how to access DICOM header information by DICOM Tag parameters. On this post I present you a quick tutorial on how to list all header information, including the Tag value, VR (value representation), Tag description, and the values of each field using the great dcm4che 2 toolkit.

(0008,0005) [CS] Specific Character Set [ISO_IR 100]
(0008,0008) [CS] Image Type [ORIGINAL]
(0008,0016) [UI] SOP Class UID [1.2.840.10008.5.1.4.1.1.2]
(0008,0020) [DA] Study Date [20040827]
(0008,0021) [DA] Series Date [20040827]
(0008,0022) [DA] Acquisition Date [20040827]
(0008,0023) [DA] Content Date [20040827]
(0008,0030) [TM] Study Time [100357.953000]
(0008,0031) [TM] Series Time [100607.062000]
(0008,0032) [TM] Acquisition Time [100622.688476]
(0008,0033) [TM] Content Time [100622.688476]
(0008,0050) [SH] Accession Number [null]
(0008,0060) [CS] Modality [CT]
(0008,0070) [LO] Manufacturer [SIEMENS]

Like the previous posts, we start coding a simple class with the default constructor. Let's name it ListDicomHeader.


public class ListDicomHeader {

public ListDicomHeader() {
// TODO Auto-generated method stub
}

public static void main(String[] args) {
// TODO Auto-generated method stub
}
}

The next step is to code the method responsible for extracting the header info. Note that this method is recursive. This is done because some DICOM files bring encoded Items that may hold other DICOM objects denoted by de value representation SQ. So, we are handling also sequence information with this code. The method is written as follows:


public void listHeader(DicomObject object) {
Iterator iter = object.datasetIterator();
while(iter.hasNext()) {
DicomElement element = iter.next();
int tag = element.tag();
try {
String tagName = object.nameOf(tag);
String tagAddr = TagUtils.toString(tag);
String tagVR = object.vrOf(tag).toString();
if (tagVR.equals("SQ")) {
if (element.hasItems()) {
System.out.println(tagAddr +" ["+ tagVR +"] "+ tagName);
listHeader(element.getDicomObject());
continue;
}
}
String tagValue = object.getString(tag);
System.out.println(tagAddr +" ["+ tagVR +"] "+ tagName +" ["+ tagValue+"]");
} catch (Exception e) {
e.printStackTrace();
}
}
}

Looking at the code, first we get an iterator to go through our DICOM dataset. Then we code a while loop to get each DICOM element present in the header. At each iteration a new DicomElement is kept so we can access its values. The tag variable holds the current Tag value. From then on there are some useful functions that may help us a lot. We can use the nameOf method from DicomObject class to get the Tag description as a String. I also suggest you to have a look at the TagUtils class for other great functions. The vrOf function will return the value representation to the current element.

Then comes the recursive part. We test the VR to see if it's a sequence (SQ), if so then we check if this element has any Items. Then if the answer is true we get the new object and call the listHeader function again, starting the recursive loop. Each iteration then prints out the desired information.

Finally, to test this program we must a main method for this class. The method may be written as follows:


public static void main(String[] args) {
DicomObject object = null;
try {
DicomInputStream dis = new DicomInputStream(new File("c:/image.dcm"));
object = dis.readDicomObject();
dis.close();
} catch (Exception e) {
System.out.println(e.getMessage());
System.exit(0);
}
ListDicomHeader list = new ListDicomHeader();
list.listHeader(object);
}


That's it! Now we have a lot of information from our DICOM file header! Enjoy :)

Best regards,

Samuel.

Tuesday, December 9, 2008

Converting JPEG to DICOM using dcm4che 2

Hi Folks!

At last my small quiz has finished. It was just a few votes (15), however, we’ve got an interesting choice: the Jpeg to Dicom tutorial. So let’s see how we can encapsulate a Jpeg image into a Dicom dataset. This tutorial is based on jpg2dcm utility from dcm4che2 toolkit and you are supposed to know basic java programming, the Eclipse environment, and some Dicom stuff.

Open your Eclipse IDE and choose File > New > Java Project. Name it myJpegToDicom. Next we have to create a new class, so right-click the src package and select New > Class. Enter JpegToDicom for class name and select the main method option. You'll get something like this:


public class JpegToDicom {
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}

I strongly recommend that you have a look at my previous posts for reviewing some configuration steps. Inside the main’s body let’s code the following lines:


File jpgSource = new File("c:/picture.jpg");
File dcmDestination = new File("c:/picture.dcm");

The first line creates a new file which is our Jpeg image, and the second one creates our future Dicom file which will keep Jpeg data within. Then, we have to extract some header information from our Jpeg file. To make it easy to understand I decided to use other means than search for Jpeg header markers and hexadecimal numbers. So, let’s do the following:


try {
BufferedImage jpegImage = ImageIO.read(jpgSource);
if (jpegImage == null)
throw new Exception("Invalid file.");

We open a try block and then read our Jpeg into a BufferedImage through ImageIO.read() method. If this results in an invalid image so we may throw a new exception. Else, we’ve got a valid image and therefore valuable information about it. The BufferedImage class has a lot of useful methods for retrieving image data, then let’s save the number of color components (samples per pixel) of our image, the bits per pixel and the bits allocated:


int colorComponents = jpegImage.getColorModel().getNumColorComponents();
int bitsPerPixel = jpegImage.getColorModel().getPixelSize();
int bitsAllocated = (bitsPerPixel / colorComponents);
int samplesPerPixel = colorComponents;

It’s time to start building our Dicom dataset:


DicomObject dicom = new BasicDicomObject();
dicom.putString(Tag.SpecificCharacterSet, VR.CS, "ISO_IR 100");
dicom.putString(Tag.PhotometricInterpretation, VR.CS, samplesPerPixel == 3 ? "YBR_FULL_422" : "MONOCHROME2");

The first line creates a new basic Dicom object defined by dcm4che2 toolkit. The next one puts header information for Specific Character Set: ISO_IR 100 – it’s the same for ISO-8859-1 – the code for Latin alphabet. Finally, the last line puts header information for photometric interpretation (read with or without colors). So if our image has samples per pixel equals to 3, it has colors (YBR_FULL_422), else it’s a grayscale image (MONOCHROME2).

The following lines add integer values to our Dicom header. Note that all of them comes from BufferedImage methods. These values are mandatory when encapsulating. For more information you can check Part 3.5 of Dicom Standard.

dicom.putInt(Tag.SamplesPerPixel, VR.US, samplesPerPixel);
dicom.putInt(Tag.Rows, VR.US, jpegImage.getHeight());
dicom.putInt(Tag.Columns, VR.US, jpegImage.getWidth());
dicom.putInt(Tag.BitsAllocated, VR.US, bitsAllocated);
dicom.putInt(Tag.BitsStored, VR.US, bitsAllocated);
dicom.putInt(Tag.HighBit, VR.US, bitsAllocated-1);
dicom.putInt(Tag.PixelRepresentation, VR.US, 0);

Also, our Dicom header needs information about date and time of creation:

dicom.putDate(Tag.InstanceCreationDate, VR.DA, new Date());
dicom.putDate(Tag.InstanceCreationTime, VR.TM, new Date());

Every Dicom file has a unique identifier. Here we’re generating study, series and Sop instances UIDs. You may want to modify these values, but you should to care about their uniqueness.

dicom.putString(Tag.StudyInstanceUID, VR.UI, UIDUtils.createUID());
dicom.putString(Tag.SeriesInstanceUID, VR.UI, UIDUtils.createUID());
dicom.putString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID());

Our Dicom header is almost done. The following command initiates Dicom metafile information considering JPEGBaseline1 as transfer syntax. This means this file has Jpeg data encapsulated instead common medical image pixel data. The most common Jpeg files use a subset of the Jpeg standard called baseline Jpeg. A baseline Jpeg file contains a single image compressed with the baseline discrete cosine transformation (DCT) and Huffman encoding.

dicom.initFileMetaInformation(UID.JPEGBaseline1);

After initiate the header we can open an output stream for saving our Dicom dataset as follows:

FileOutputStream fos = new FileOutputStream(dcmDestination);
BufferedOutputStream bos = new BufferedOutputStream(fos);
DicomOutputStream dos = new DicomOutputStream(bos);
dos.writeDicomFile(dicom);

The next three lines are the most important ones. According to Dicom Standard the data element PixelData (7FE0,0010), if encapsulated, has the value representation OB (Other Byte String) and its length shall be set to an undefined value (in this case -1). It also contains the encoded pixel data stream fragmented into one or more item(s):

dos.writeHeader(Tag.PixelData, VR.OB, -1);

The Item Tag (FFFE,E000) is followed by a 4 byte item length field encoding the explicit number of bytes of the item. The first item in the sequence of items before the encoded pixel data stream shall be a basic item with length equals to zero:

dos.writeHeader(Tag.Item, null, 0);

The next Item then keeps the length of our Jpeg file.

/*
According to Gunter from dcm4che team we have to take care that
the pixel data fragment length containing the JPEG stream has
an even length.
*/
int jpgLen = (int) jpgSource.length();
dos.writeHeader(Tag.Item, null, (jpgLen+1)&~1);

Now all we have to do is to fill this item with bytes taken from our Jpeg file:

FileInputStream fis = new FileInputStream(jpgSource);
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);

byte[] buffer = new byte[65536];
int b;
while ((b = dis.read(buffer)) > 0) {
dos.write(buffer, 0, b);
}

Finally, the Dicom Standard tells that we have to put a last Tag: a Sequence Delimiter Item (FFFE,E0DD) with length equals to zero.

/*
According to Gunter from dcm4che team we have to take care that
the pixel data fragment length containing the JPEG stream has
an even length. So if needed the line below pads JPEG stream with
odd length with 0 byte.
*/
if ((jpgLen&1) != 0) dos.write(0);
dos.writeHeader(Tag.SequenceDelimitationItem, null, 0);
dos.close();
} catch(Exception e) {
System.out.println("ERROR: "+ e.getMessage());
}

That’s it! Now we have a Dicom file with Jpeg data encapsulated. The print screen below was taken from Sante Free Dicom Viwer and shows a Windows Vista sample Jpeg picture encapsulated into a Dicom file.



References:

Dicom Standard Part 3.5 - Annex A (Normative) Transfer Syntax Specifications - A4 Transfer Syntaxes for Encapsulation of Encoded Pixel Data.

Jpeg Huffman Coding Tutorial

I hope it helps!

Regards,

Samuel.

Monday, September 29, 2008

Processing DICOM Images using dcm4che 2

Hi All,

First of all I want to thank all of you visitors! This post shows how we can use dcm4che2 toolkit for performing image processing. Most of articles and tutorials on the web present only pixel data accessing and manipulation, but not Dicom to Dicom image processing. Here we will manipulate an original Dicom image and then save it as a new Dicom processed image. You're supposed to know basic Java programming, the Eclipse environment and some Dicom stuff.


Lets start. Open your Eclipse IDE and choose File > New > Java Project. Name it myDicomImageProcessing. The next step is to create a new class, so right-click the src package and select New > Class. Enter DicomImageProcessing for class name and select the main method option. You'll get something like this:

public class DicomImageProcessing {
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}

I encourage you to have a look at my last post Converting DICOM to JPEG because there are some repeated code here which there is no need to be explained again. Inside the main's body let's code the following line:

File sourceFile = new File("c:/original.dcm");

This will be our original Dicom file. Next we will open this file and get its pixel data:

try {
Iterator iter = ImageIO.getImageReadersByFormatName("DICOM");
ImageReader reader = (ImageReader) iter.next();
DicomImageReadParam param = (DicomImageReadParam) reader.getDefaultReadParam();
ImageInputStream iis = ImageIO.createImageInputStream(sourceFile);

reader.setInput(iis, false);
Raster raster = reader.readRaster(0, param);

Our ImageReader is able to return a complete Raster from our Dicom image. According to Java documentation a Raster represents values for pixels occupying a particular rectangular area (read pixel array). Then, if our raster object is null we got some errors opening the file, so we may display an error message:

if (raster == null) {
System.out.println("Error: couldn't read Dicom image!");
return;
}
iis.close();

Ok, we read the file and our raster object holds all pixel data information. Now comes the image processing tour. For this example I chose Java ConvolveOp class for performing blurring, embossing and sharpening operations. It implements a convolution from the source to the destination.

Convolution using a convolution kernel is a spatial operation that computes the output pixel from an input pixel by multiplying the kernel with the surround of the input pixel. This allows the output pixel to be affected by the immediate neighborhood in a way that can be mathematically specified with a kernel.

Great, we also need to use the Kernel class. This class defines a matrix that describes how a specified pixel and its surrounding pixels affect the value computed for the pixel's position in the output image of a filtering operation.

Now let's create a new kernel for applying an emboss filter to our Dicom image:

float[] emboss = new float[] { -2,0,0, 0,1,0, 0,0,2 };
Kernel kernel = new Kernel(3, 3, emboss);

See that our kernel is a simple matrix. You can change this values further and check the results, you are the man :) Next let's apply the filter through our ConvolveOp class.

ConvolveOp op = new ConvolveOp(kernel);
Raster newRaster = op.filter(raster, null);

Note that, we just pass our kernel as a parameter and create a new ConvolveOp object. The filter(src,dst) method can return a BufferedImage or a new Raster object containing all the manipulated pixel data. So we have to choose the second option. See that we set null to the raster destination parameter, this force the creation of a new Raster as a result. That's it! Our Dicom image was processed!

In order to save a new Dicom file that contains our processed image, we have to do the following:
1) Extract the array element from the raster DataBuffer;
2) Create a Dicom object without pixel data;
3) Add to the Dicom object the pixel data extracted from the DataBuffer;
4) Write the modified Dicom object to a file;

So let's go! In this example I use an 16Bit MR Dicom image and its raster holds a DataBufferUShort object, so we have 16Bit pixels stored on short type arrays. Depending on the file you may get a DataBufferByte object inside the raster and you'll have to adapt my example code. To extract the array just code the following lines:

DataBufferUShort buffer = (DataBufferUShort) newRaster.getDataBuffer();
short[] pixelData = buffer.getData();

The next step is to create a copy of our Dicom file without pixel data. This can be done by calling the getDicomObject() method from the DicomStreamMetaData class.

DicomStreamMetaData dsmd = (DicomStreamMetaData) reader.getStreamMetadata();
DicomObject dicom = dsmd.getDicomObject();

Allright, we got the Dicom object! All we have to do so is to add our modified pixel data inside this object. We need to access the PixelData tag and insert our pixelData short array like the code below:

dicom.putShorts(Tag.PixelData, dicom.vrOf(Tag.PixelData), pixelData);

Finally, lets save our new Dicom file containing the processed image:

File f = new File("C:/processed.dcm");
FileOutputStream fos = new FileOutputStream(f);
BufferedOutputStream bos = new BufferedOutputStream(fos);
DicomOutputStream dos = new DicomOutputStream(bos);
dos.writeDicomFile(dicom);
dos.close();

And, if we get any errors let's display a simple message:

} catch(Exception e) {
System.out.println("Error: couldn't read dicom image! "+ e.getMessage());
return;
}
}

It should work! I successfully opened the processed.dcm file on ImageJ. In addition, you may want to test other processing kernels and save different sets of processed images. You can try the following:

float[] blurring = new float[] { 1f/9f,1f/9f,1f/9f, 1f/9f,1f/9f,1f/9f, 1f/9f,1f/9f,1f/9f };
float[] sharpening = new float[] { -1,-1,-1, -1,9,-1, -1,-1,-1 };

If you have any problems running my examples just let me know!

Best regards,

Samuel.