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.