Wednesday, February 23, 2011

Creating DICOM Multiframe files (MPEG to DICOM) using dcm4che 2

Hi There!

Long break, uh? Well, dear visitors, I have received a lot of emails asking about multiframe DICOM files. This time we will see an approach on how to build such files. A multiframe DICOM file usually has the meaning of a video file. You can open it in a DICOM viewer and then playback its frames.

To achieve such behavior we will need to know how to extract image frames from a MPEG-2 video file. FFMPEG program is a wonderful tool and complete, cross-platform solution to record, convert and stream audio and video. Also you can get Windows versions of FFMPEG here or here. Once your download is done unpack the FFMPEG file to you root directory and type the following command in your command prompt window:

C:\ffmpeg\bin> ffmpeg -i yourvideo.mpg -r 30 -s 320x240 -f image2 c:/temp/images%05d.png

According to FFMPEG documentation we are trying an input file (-i) called yourvideo.mpg to extract and save 30 image frames per second (-r). Each extracted image will have 320 pixels width and 240 pixels height (-s). Finally, each frame is saved as PNG images to the Windows Temp directory (-f). Below you can see an example of a successful FFMPEG output to our command.



Check your Temp directory to see your extracted frames. Depending your video length you will get thousands of frames :) Now all we need to do is to loop through all PNG frames, read and encode them as DICOM frames. Using the dcm4che toolkit, the following Java class does the trick. Please, read the comments in the source code to understand what is going on.

Happy coding!

Samuel.

package com.samucsdev.util;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;

import org.dcm4che2.data.BasicDicomObject;
import org.dcm4che2.data.DicomObject;
import org.dcm4che2.data.Tag;
import org.dcm4che2.data.UID;
import org.dcm4che2.data.VR;
import org.dcm4che2.imageio.plugins.dcm.DicomStreamMetaData;
import org.dcm4che2.imageioimpl.plugins.dcm.DicomImageWriterSpi;
import org.dcm4che2.util.UIDUtils;

/**
 * MPEG-2 to DICOM Multiframe.
 * 
 * @author Samuel Covas Salomao (samucs@gmail.com)
 */
public class Mpeg2Dicom {
 
    /**
     * Class constructor.
     */
    public Mpeg2Dicom() {
  
    }

    /**
     * Create the DICOM multiframe file header.
     * 
     * @param sampleFrame
     *            a sample BufferedImage to get image information.
     * @param numberOfFrames
     *            the number of frames of this multiframe DICOM file.
     */
    public DicomObject createDicomHeader(BufferedImage sampleFrame, int numberOfFrames) {

        // Get some image information from the sample image:
        // All frames should have the same information so we will get it only once.
        int colorComponents = sampleFrame.getColorModel().getNumColorComponents();
        int bitsPerPixel = sampleFrame.getColorModel().getPixelSize();
        int bitsAllocated = (bitsPerPixel / colorComponents);
        int samplesPerPixel = colorComponents;
  
        // The DICOM object that will hold our frames
        DicomObject dicom = new BasicDicomObject();
  
        // Add patient related information to the DICOM dataset
        dicom.putString(Tag.PatientName, null, "SAMUCS^DEV");
        dicom.putString(Tag.PatientID, null, "1234ID");
        dicom.putDate(Tag.PatientBirthDate, null, new java.util.Date());
        dicom.putString(Tag.PatientSex, null, "M");
  
        // Add study related information to the DICOM dataset
        dicom.putString(Tag.AccessionNumber, null, "1234AC");
        dicom.putString(Tag.StudyID, null, "1");
        dicom.putString(Tag.StudyDescription, null, "MULTIFRAME STUDY");
        dicom.putDate(Tag.StudyDate, null, new java.util.Date());
        dicom.putDate(Tag.StudyTime, null, new java.util.Date());

        // Add series related information to the DICOM dataset
        dicom.putInt(Tag.SeriesNumber, null, 1);
        dicom.putDate(Tag.SeriesDate, null, new java.util.Date());
        dicom.putDate(Tag.SeriesTime, null, new java.util.Date());
        dicom.putString(Tag.SeriesDescription, null, "MULTIFRAME SERIES");
        dicom.putString(Tag.Modality, null, "SC"); // secondary capture

        // Add image related information to the DICOM dataset
        dicom.putInt(Tag.InstanceNumber, null, 1);
        dicom.putInt(Tag.SamplesPerPixel, null, samplesPerPixel);
        dicom.putString(Tag.PhotometricInterpretation, VR.CS, "YBR_FULL_422");
        dicom.putInt(Tag.Rows, null, sampleFrame.getHeight());
        dicom.putInt(Tag.Columns, null, sampleFrame.getWidth());
        dicom.putInt(Tag.BitsAllocated, null, bitsAllocated);
        dicom.putInt(Tag.BitsStored, null, bitsAllocated);
        dicom.putInt(Tag.HighBit, null, bitsAllocated-1);
        dicom.putInt(Tag.PixelRepresentation, null, 0);

        // Add the unique identifiers
        dicom.putString(Tag.SOPClassUID, null, UID.SecondaryCaptureImageStorage);
        dicom.putString(Tag.StudyInstanceUID, null, UIDUtils.createUID());
        dicom.putString(Tag.SeriesInstanceUID, null, UIDUtils.createUID());
        dicom.putString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID());

        //Start of multiframe information:
        dicom.putInt(Tag.StartTrim, null, 1);                   // Start at frame 1
        dicom.putInt(Tag.StopTrim, null, numberOfFrames);       // Stop at frame N
        dicom.putString(Tag.FrameTime, null, "33.33");          // Milliseconds (30 frames per second)
        dicom.putString(Tag.FrameDelay, null, "0.0");           // No frame dalay
        dicom.putInt(Tag.NumberOfFrames, null, numberOfFrames); // The number of frames
        dicom.putInt(Tag.RecommendedDisplayFrameRate, null, 3);  
        dicom.putInt(Tag.FrameIncrementPointer, null, Tag.FrameTime);
        //End of multiframe information.

        // Add the default character set
        dicom.putString(Tag.SpecificCharacterSet, VR.CS, "ISO_IR 100");

        // Init the meta information with JPEG Lossless transfer syntax
        dicom.initFileMetaInformation(UID.JPEGLossless);
  
        return dicom;
    }
 
    /**
     * Encode the extracted FFMPEG frames to DICOM Sequence instances.
     * 
     * @param frames
     *            array of files as input (PNG images).
     * @param dest
     *            the destination directory to save the multiframe DICOM file.
     */
     public void encodeMultiframe(File[] frames, File dest) 
     throws IOException {
        // Status message
        System.out.println("Creating Multiframe File...");
  
        // Create DICOM image writer instance and set its output
        ImageWriter writer = new DicomImageWriterSpi().createWriterInstance();
        FileImageOutputStream output = new FileImageOutputStream(dest);
        writer.setOutput(output);
  
        // Get an image sample from the array of images
        BufferedImage sample = ImageIO.read(frames[0]);
  
        // Create a new dataset (header/metadata) for our DICOM image writer
        DicomObject ds = this.createDicomHeader(sample, frames.length);

        // Set the metadata to our DICOM image writer and prepare to encode the multiframe sequence
        DicomStreamMetaData writeMeta = (DicomStreamMetaData) writer.getDefaultStreamMetadata(null);
        writeMeta.setDicomObject(ds);
        writer.prepareWriteSequence(writeMeta);
  
        // Status message
        System.out.println("Start of Write Sequence...");
  
        // For each extracted FFMPEG images...
        for (int i = 0; i < frames.length; i++) {
   
            // Status message
            System.out.println("Encoding frame # "+ (i+1));
   
            // Read the PNG file to a BufferedImage object
            BufferedImage frame = ImageIO.read(frames[i]);
   
            // Create a new IIOImage to be saved to the DICOM multiframe sequence
            IIOImage iioimage = new IIOImage(frame, null, null);
   
            // Write our image to the DICOM multiframe sequence
            writer.writeToSequence(iioimage, null);
        }

        // Status message
        System.out.println("End of Write Sequence.");

        // Our multiframe file was created. End the sequence and close the output stream.
        writer.endWriteSequence();
        output.close();

        // Status message
        System.out.println("Multiframe File Created.");
    }
 
    /**
     * Run the program.
     * 
     * @param args
     *            program arguments.
     */
    public static void main(String[] args) {  
        try {
            // Create an instance of our class
            Mpeg2Dicom f = new Mpeg2Dicom();
   
            // Create the array of files for the extracted FFMPEG images
            File[] frames = new File("C:/temp/input").listFiles();
   
            // Create the DICOM multiframe file
            f.encodeMultiframe(frames, new File("C:/temp/output/multiframe.dcm"));
   
        } catch (Exception e) {
            // Print exceptions
            e.printStackTrace();
        }
    }
}


16 comments:

Nabo said...

Hi,
rly liked your blog =). But this code isn't working here...
i tried to use your code exactly (i change the folder...) like it's here, but it's giving an error, more specific:

org.dcm4che2.data.ConfigurationError: No Image Writer of class com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriter available for format:jpeg-lossless
at org.dcm4che2.imageio.ImageWriterFactory.getWriterForTransferSyntax(ImageWriterFactory.java:94)
at org.dcm4che2.imageioimpl.plugins.dcm.DicomImageWriter.setupWriter(DicomImageWriter.java:219)
at org.dcm4che2.imageioimpl.plugins.dcm.DicomImageWriter.prepareWriteSequence(DicomImageWriter.java:243)
at Mpeg2Dicom.encodeMultiframe(Mpeg2Dicom.java:137)
at Mpeg2Dicom.main(Mpeg2Dicom.java:184)

i created a folder with png images and executed the code.
Do you have some idea of what this error is?

Thx.

Unknown said...

Hi Nabo,

In order to work with compressed images dcm4che needs the JAI ImageIO library. Install the library and try again. For instance it may not work on Mac OSX.

Regards,

Samuel.

Nabo said...

Hello Samuel,
thanks for being answering me, as you said i downloaded the jai library (at least i think), i imported the jai_imageio.jar, jai_core.jar and jai_codec.jar in the project.
And instaled from this link too: http://migre.me/5vLuW and instaled like says here: http://migre.me/5vLvT.
None of the methods worked...

I use windows 7 64 bits, may my windows be the problem.

Thanks again,

Ricardo

kkl said...

Hi, I have the issue stated by Nabo too. How do we resolve that?

Anonymous said...

As far as I know JAI ImageIO dos not work in windows 64.....

Unknown said...

Olá Samuel,

parabéns pelo blog, as informações são excelentes. Estou trabalhando em um projeto acadêmico com imagens DICOM, e utilizando a biblioteca dcm4che para acessar essas imagens. Até então utilizava o seguinte código para ter acesso as imagens:

DicomObject dcmObj;
DicomInputStream din = null;
din = new DicomInputStream(new File("c:\\IM.dcm"));
dcmObj = din.readDicomObject();
din.close();
short [] data = dcmObj.getShorts(Tag.PixelData);


Esse código funcionou perfeitamente para diversas imagens, porém ultimamente algumas imagens estão sendo carregadas com valores errados (Ex.: os valores da região do pulmão geralmente são negativos, e para alguns exames estes ficam positivos). Alguma ideia do que possa estar acontecendo?
Desde já agradeço pela atenção.
Tarique

Anonymous said...

Hi Samuel,

I have to cretae sc dicom object with jpeg image.I am also getting same error as Nabo mentioned.
I am using windows 7.Pls sugges me .

Kiran

Anonymous said...

Hello Samuel,

Excellent blog - you have helped me a number of times with helpful DCM4CHE knowledge!

To those having issues with no Image Reader / Writer for JPEG, you can absolutely use Windows 64-bit... but you will need to have a 32-bit Java with JAI. The native libraries JAI requires are only built for 32-bit windows.

Ranjan said...

can i get it same after using DCMTK?

Anonymous said...

Hi samucs-dev

I've try your code, with the same video , with the same code (nothing changed) with the same ffmpeg setting and try to oper dcm file with the same Dicom viewer(sante free dicom viewer) but not work!
The viewer tell me cannot open the file!

Anonymous said...

Hi,

The line:
dicom.putString(Tag.PhotometricInterpretation, VR.CS, "YBR_FULL_422");

should be
dicom.putString(Tag.PhotometricInterpretation, VR.CS, "RGB");

The output of the ffmpeg decoder is RGB, and this header value must match.

Anonymous said...

Regarding the last PhotometricInterpretation comment - the DICOM PhotometricInterpretation will depend on how ffmpeg outputs the image data. Depending on the source (MPEG vs. AVI, etc.), and the output file format (JPG vs PNG vs RAW), you may get different colorspaces. I can't figure out from the ffmpeg documentation what that colorspace will always be.

However, I think that PNG should be RGB . Certainly I've seen that happen in one case of using this code. YBR_FULL_422 is a photometric that can only exist in DICOM within a JPEG-encoded image.

Unknown said...
This comment has been removed by the author.
Anonymous said...

Hello guys any suggestions, how to achieve this on 64 bit JAVA since we are using 64-bit java in project?

Unknown said...

I am writing a dcm4che 3 port of this. And will share that on the internet. Thank you for a great starting point. I have done this 60 times in the past in different languages but still good to have something to start with.

Prakash Boda said...

If you are trying to open converted file into viewer and it gives error that it can not open file then most probably you are missing Planar Configuration tag which is required if Samples per pixel is more than 1.
https://dicom.innolitics.com/ciods/enhanced-mr-image/enhanced-mr-image/00280006