DICOM Basics using .NET and C# - Extracting Image Data

Introduction

This is part of my series of articles on the DICOM standard. In this tutorial, we'll learn how to extract pixel data from DICOM files and export them to standard image formats like JPEG, PNG, and BMP using .NET and the fo-dicom library.

Extracting image data from DICOM files is a common requirement in medical imaging applications. Whether you're building a web-based viewer that needs to display images in standard formats, creating thumbnails for a patient worklist, or exporting images for reports, understanding how to properly extract and render DICOM images is essential.

Prerequisites

Before you begin, ensure you have the following:

“The real voyage of discovery consists not in seeking new landscapes, but in having new eyes.” ~ Marcel Proust

Key Image Attributes

Before extracting image data, it's important to understand the key DICOM attributes that define how pixel data is stored:

TagNameDescription
(0028,0010)RowsImage height in pixels
(0028,0011)ColumnsImage width in pixels
(0028,0100)Bits Allocated8, 16, or 32 bits per pixel
(0028,0101)Bits StoredActual bits used for pixel values
(0028,0004)Photometric InterpretationMONOCHROME1/2, RGB
(0028,1050)Window CenterBrightness control (Level)
(0028,1051)Window WidthContrast control
(7FE0,0010)Pixel DataRaw pixel bytes

Step 1 of 3: Reading DICOM File and Image Parameters

First, let's open a DICOM file and display its image parameters to understand what we're working with:

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using FellowOakDicom;
using FellowOakDicom.Imaging;

namespace ExtractingImageData
{
    public class Program
    {
        private static readonly string PathToDicomTestFile =
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test Files", "0002.dcm");
        private static readonly string OutputPath =
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Output");

        public static void Main(string[] args)
        {
            try
            {
                LogToDebugConsole("=== Extracting DICOM Image Data Tutorial ===");
                LogToDebugConsole($"Reading DICOM file: {PathToDicomTestFile}");

                // Ensure output directory exists
                if (!Directory.Exists(OutputPath))
                {
                    Directory.CreateDirectory(OutputPath);
                }

                // Open the DICOM file
                var file = DicomFile.Open(PathToDicomTestFile);
                var dataset = file.Dataset;

                // Display Image Parameters
                LogToDebugConsole("--- Image Parameters ---");

                var rows = dataset.GetSingleValueOrDefault(DicomTag.Rows, (ushort)0);
                var columns = dataset.GetSingleValueOrDefault(DicomTag.Columns, (ushort)0);
                var bitsAllocated = dataset.GetSingleValueOrDefault(DicomTag.BitsAllocated, (ushort)0);
                var bitsStored = dataset.GetSingleValueOrDefault(DicomTag.BitsStored, (ushort)0);
                var samplesPerPixel = dataset.GetSingleValueOrDefault(DicomTag.SamplesPerPixel, (ushort)0);
                var photometricInterpretation = dataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, "");
                var windowCenter = dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, 0.0);
                var windowWidth = dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, 0.0);

                LogToDebugConsole($"  Dimensions:           {columns} x {rows} pixels");
                LogToDebugConsole($"  Bits Allocated:       {bitsAllocated}");
                LogToDebugConsole($"  Bits Stored:          {bitsStored}");
                LogToDebugConsole($"  Samples Per Pixel:    {samplesPerPixel} ({(samplesPerPixel == 1 ? "Grayscale" : "Color")})");
                LogToDebugConsole($"  Photometric:          {photometricInterpretation}");
                LogToDebugConsole($"  Window Center:        {windowCenter}");
                LogToDebugConsole($"  Window Width:         {windowWidth}");
            }
            catch (Exception e)
            {
                LogToDebugConsole($"Error: {e.Message}");
            }
        }

        private static void LogToDebugConsole(string message)
        {
            Debug.WriteLine(message);
        }
    }
}

Step 2 of 3: Rendering DICOM Images

The fo-dicom library provides the DicomImage class which handles the complexity of rendering DICOM pixel data, including applying window/level settings and handling different photometric interpretations:

// Create a DicomImage for rendering
var dicomImage = new DicomImage(PathToDicomTestFile);

// Get the number of frames (for multi-frame images like cine)
var frameCount = dicomImage.NumberOfFrames;
LogToDebugConsole($"  Number of Frames:     {frameCount}");

// Render and process frames
for (int frame = 0; frame < frameCount; frame++)
{
    // Render the image to a bitmap
    var renderedImage = dicomImage.RenderImage(frame);
    var bitmap = renderedImage.As<Bitmap>();

    // Process the bitmap (shown in next step)
    // ...

    // Clean up bitmap when done
    bitmap.Dispose();
}

Step 3 of 3: Exporting to Standard Image Formats

Once we have the rendered bitmap, we can save it to various standard image formats:

LogToDebugConsole("--- Exporting Image ---");

// Create a DicomImage for rendering
var dicomImage = new DicomImage(PathToDicomTestFile);

// Get the number of frames
var frameCount = dicomImage.NumberOfFrames;
LogToDebugConsole($"  Number of Frames:     {frameCount}");

// Render each frame (or just the first for single-frame images)
for (int frame = 0; frame < frameCount; frame++)
{
    // Render the image to a bitmap
    var renderedImage = dicomImage.RenderImage(frame);
    var bitmap = renderedImage.As<Bitmap>();

    // Generate output filenames
    var frameLabel = frameCount > 1 ? $"_frame_{frame + 1:D3}" : "";
    var jpegPath = Path.Combine(OutputPath, $"exported_image{frameLabel}.jpg");
    var pngPath = Path.Combine(OutputPath, $"exported_image{frameLabel}.png");
    var bmpPath = Path.Combine(OutputPath, $"exported_image{frameLabel}.bmp");

    // Save in multiple formats
    bitmap.Save(jpegPath, ImageFormat.Jpeg);
    bitmap.Save(pngPath, ImageFormat.Png);
    bitmap.Save(bmpPath, ImageFormat.Bmp);

    if (frameCount > 1)
    {
        LogToDebugConsole($"  Exported frame {frame + 1}/{frameCount}");
    }
    else
    {
        LogToDebugConsole($"  JPEG: {jpegPath}");
        LogToDebugConsole($"  PNG:  {pngPath}");
        LogToDebugConsole($"  BMP:  {bmpPath}");
    }

    // Clean up bitmap
    bitmap.Dispose();
}

LogToDebugConsole("Image export completed successfully!");

The output will show the extracted image parameters and confirm the export:

=== Extracting DICOM Image Data Tutorial ===
Reading DICOM file: C:\...\Test Files\0002.dcm

--- Image Parameters ---
  Dimensions:           512 x 512 pixels
  Bits Allocated:       8
  Bits Stored:          8
  Samples Per Pixel:    1 (Grayscale)
  Photometric:          MONOCHROME2
  Window Center:        128
  Window Width:         256

--- Exporting Image ---
  Number of Frames:     96
  Exported frame 1/96
  Exported frame 2/96
  ...
  Exported frame 96/96

Image export completed successfully!

Understanding Photometric Interpretation

The Photometric Interpretation attribute tells us how to interpret the pixel values:

ValueDescription
MONOCHROME1Minimum pixel value is white
MONOCHROME2Maximum pixel value is white (most common)
RGBColor image with red, green, blue samples
YBR_FULLYCbCr color space
PALETTE COLORIndexed color with lookup table

Handling Multi-Frame Images

Multi-frame DICOM images (cine loops) contain multiple images in a single file. The NumberOfFrames property indicates how many frames are present. When exporting, you can either:

  • Export each frame as a separate image file
  • Create an animated GIF or video from the frames
  • Extract only specific frames of interest

Conclusion

In this tutorial, we've learned how to extract pixel data from DICOM files and export them to standard image formats using .NET and the fo-dicom library. We covered reading image parameters, rendering DICOM images with proper window/level settings, and saving to JPEG, PNG, and BMP formats.

This capability is essential for building medical imaging applications that need to display images in web browsers, create reports with embedded images, or integrate with non-DICOM systems.

Please check out the next tutorial in this series where we cover viewing DICOM images with window/level adjustments.