Click here to Skip to main content
15,892,746 members
Articles / Desktop Programming / WPF

Medical image visualization using WPF

Rate me:
Please Sign up or sign in to vote.
4.96/5 (68 votes)
27 Sep 2012CPOL6 min read 109.6K   7.7K   121  
The article demonstrates the visualization of medical images (DICOM) using WPF.
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Xml.Linq;


namespace DICOMViewer.Helper
{
    // The CTSliceInfo class is a wrapper class in order to provide all important CT Image information.

    public class CTSliceInfo
    {
        private XDocument mXDocument = null;
        private string mFileName = null;

        private int mColumnCount = -1;
        private int mRowCount = -1;
        private double mUpperLeft_X = -1;
        private double mUpperLeft_Y = -1;
        private double mUpperLeft_Z = -1;
        private double mPixelSpacing_X = -1;
        private double mPixelSpacing_Y = -1;
        
        private int[,] mHounsfieldPixelBuffer = null;
        private WriteableBitmap mBitmap = null;

        // Public properties
        public int ColumnCount { get { return mColumnCount; } }
        public int RowCount { get { return mRowCount; } }
        public double UpperLeft_X { get { return mUpperLeft_X; } }
        public double UpperLeft_Y { get { return mUpperLeft_Y; } }
        public double UpperLeft_Z { get { return mUpperLeft_Z; } }
        public double PixelSpacing_X { get { return mPixelSpacing_X; } }
        public double PixelSpacing_Y { get { return mPixelSpacing_Y; } }

        public CTSliceInfo(XDocument theXDocument, string theFileName)
        {
            mXDocument = theXDocument;
            mFileName = theFileName;

            mColumnCount = DICOMParserUtility.GetDICOMAttributeAsInt(theXDocument, "(0028,0011)");
            mRowCount = DICOMParserUtility.GetDICOMAttributeAsInt(theXDocument, "(0028,0010)");

            // DICOM attribute 'Image Position Patient' (0020,0032)
            string aImagePositionPatientString = DICOMParserUtility.GetDICOMAttributeAsString(theXDocument, "(0020,0032)");
            string[] aImagePositionPatientArray = aImagePositionPatientString.Split('\\');
            mUpperLeft_X = Convert.ToDouble(aImagePositionPatientArray[0], CultureInfo.InvariantCulture);
            mUpperLeft_Y = Convert.ToDouble(aImagePositionPatientArray[1], CultureInfo.InvariantCulture);
            mUpperLeft_Z = Convert.ToDouble(aImagePositionPatientArray[2], CultureInfo.InvariantCulture);

            // DICOM attribute 'Pixel Spacing' (0028,0030)
            string aPixelSpacingString = DICOMParserUtility.GetDICOMAttributeAsString(theXDocument, "(0028,0030)");
            string[] aPixelSpacingValueArray = aPixelSpacingString.Split('\\');
            mPixelSpacing_X = Convert.ToDouble(aPixelSpacingValueArray[0], CultureInfo.InvariantCulture);
            mPixelSpacing_Y = Convert.ToDouble(aPixelSpacingValueArray[1], CultureInfo.InvariantCulture);
        }

        // This method has to be called before the marching cubes algorithm is called.
        // The method shifts the x/y/z position of the CT slice by the expected center point of the 3D model.
        // By doing so, the resulting 3D model will be centered in the origin of the coordinate system.
        public void AdjustPatientPositionToCenterPoint(Point3D theCenterPoint)
        {
            mUpperLeft_X -= theCenterPoint.X;
            mUpperLeft_Y -= theCenterPoint.Y;
            mUpperLeft_Z -= theCenterPoint.Z;
        }

        // Helper method, which will return the Hounsfield value of a specified voxel (RowIndex/ColumnIndex).
        public int GetHounsfieldPixelValue(int theRowIndex, int theColumnIndex)
        {
            // Encoding of the pixel buffer is only done for the first call.
            if (mHounsfieldPixelBuffer == null)
            {
                int aRescaleIntercept = DICOMParserUtility.GetDICOMAttributeAsInt(mXDocument, "(0028,1052)");
                int aRescaleSlope = DICOMParserUtility.GetDICOMAttributeAsInt(mXDocument, "(0028,1053)");
                bool aPixelPaddingValueExist = DICOMParserUtility.DoesDICOMAttributeExist(mXDocument, "(0028,0120)");
                int aPixelPaddingValue = DICOMParserUtility.GetDICOMAttributeAsInt(mXDocument, "(0028,0120)");

                // Allocate the Hounsfield Pixel Buffer
                mHounsfieldPixelBuffer = new int[mRowCount, mColumnCount];

                // Find the pixel data DICOM attribute (7FE0,0010)
                var aPixelDataQuery = from Element in mXDocument.Descendants("DataElement")
                                      where Element.Attribute("Tag").Value.Equals("(7FE0,0010)")
                                      select Element;

                // Get the start position of the stream for the pixel data attribute 
                long aStreamPosition = Convert.ToInt64(aPixelDataQuery.Last().Attribute("StreamPosition").Value);

                // Open the binary reader
                BinaryReader aBinaryReader = new BinaryReader(File.Open(mFileName, FileMode.Open, FileAccess.Read, FileShare.Read));
                
                // Set the stream position of the binary reader to first pixel
                aBinaryReader.BaseStream.Position = aStreamPosition;

                // Loop over all pixel data values
                for (int aRowIndex = 0; aRowIndex < mRowCount; aRowIndex++)
                {
                    for (int aColumnIndex = 0; aColumnIndex < mColumnCount; aColumnIndex++)
                    {
                        // For some images, the pixel buffer is smaller than '2Byte * RowCount * ColumnCount'
                        // That's why we need the check...
                        if(aBinaryReader.BaseStream.Position - 2 < aBinaryReader.BaseStream.Length)
                        {
                            byte aByte0 = aBinaryReader.ReadByte();
                            byte aByte1 = aBinaryReader.ReadByte();

                            int aPixelValue = Convert.ToInt32((aByte1 << 8) + aByte0);

                            // Check for Pixel Padding Value  
                            if (aPixelPaddingValueExist)
                                if (aPixelValue == aPixelPaddingValue)
                                    aPixelValue = Int16.MinValue;

                            // Rescale handling
                            aPixelValue = aPixelValue * aRescaleSlope + aRescaleIntercept;

                            // Value of the voxel is stored in Hounsfield Units
                            mHounsfieldPixelBuffer[aRowIndex, aColumnIndex] = aPixelValue;
                        }
                    }
                }
            }

            return mHounsfieldPixelBuffer[theRowIndex, theColumnIndex];
        }

        // Helper method, which returns the pixel data of a CT slice as gray-scale bitmap.
        public WriteableBitmap GetPixelBufferAsBitmap()
        {
            if (mBitmap == null)
            {
                int aWindowCenter = DICOMParserUtility.GetDICOMAttributeAsInt(mXDocument, "(0028,1050)");
                int aWindowWidth = DICOMParserUtility.GetDICOMAttributeAsInt(mXDocument, "(0028,1051)");
                int aWindowLeftBorder = aWindowCenter - (aWindowWidth / 2);

                byte[,] aNormalizedPixelBuffer = new byte[mRowCount, mColumnCount];

                // Normalize the Pixel value to [0,255]
                for (int aRowIndex = 0; aRowIndex < mRowCount; aRowIndex++)
                {
                    for (int aColumnIndex = 0; aColumnIndex < mColumnCount; aColumnIndex++)
                    {
                        int aPixelValue = GetHounsfieldPixelValue(aRowIndex, aColumnIndex);
                        int aPixelValueNormalized = (255 * (aPixelValue - aWindowLeftBorder)) / aWindowWidth;

                        if (aPixelValueNormalized <= 0)
                            aNormalizedPixelBuffer[aRowIndex, aColumnIndex] = 0;
                        else
                            if (aPixelValueNormalized >= 255)
                                aNormalizedPixelBuffer[aRowIndex, aColumnIndex] = 255;
                            else
                                aNormalizedPixelBuffer[aRowIndex, aColumnIndex] = Convert.ToByte(aPixelValueNormalized);
                    }
                }

                // Allocate the Pixel Array for the Bitmap (4 Byte: R, G, B, Alpha value)
                byte[] aImageDataArray = new byte[mRowCount * mColumnCount * 4];

                int i = 0;
                for (int aRowIndex = 0; aRowIndex < mRowCount; aRowIndex++)
                {
                    for (int aColumnIndex = 0; aColumnIndex < mColumnCount; aColumnIndex++)
                    {
                        byte aGrayValue = aNormalizedPixelBuffer[aRowIndex, aColumnIndex];

                        // Black/White image: all RGB values are set to same value
                        // Alpha value is set to 255
                        aImageDataArray[i * 4] = aGrayValue;
                        aImageDataArray[i * 4 + 1] = aGrayValue;
                        aImageDataArray[i * 4 + 2] = aGrayValue;
                        aImageDataArray[i * 4 + 3] = 255;

                        i++;
                    }
                }

                // Allocate the Bitmap
                mBitmap = new WriteableBitmap(mColumnCount, mRowCount, 96, 96, PixelFormats.Pbgra32, null);

                // Write the Pixels
                Int32Rect anImageRectangle = new Int32Rect(0, 0, mColumnCount, mRowCount);
                int aImageStride = mColumnCount * mBitmap.Format.BitsPerPixel / 8;
                mBitmap.WritePixels(anImageRectangle, aImageDataArray, aImageStride, 0);
            }

            return mBitmap;
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Siemens Healthcare
Germany Germany
Currently working as a Requirement Engineer for Siemens Healthcare in Germany.

Comments and Discussions