DICOM Basics using .NET and C# - Presentation States (GSPS)

Introduction

This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore DICOM Presentation States, which allow you to store display preferences separately from image data. This enables consistent image display across different workstations and over time.

Presentation States store information like window width/level settings, annotations, shutters, and spatial transformations. When a viewer loads both an image and its presentation state, it can recreate the exact display settings used when the state was created.

Prerequisites

Before you begin, ensure you have the following:

  • A .NET development environment (Visual Studio or Visual Studio Code)
  • The Fellow Oak DICOM library (fo-dicom) installed via NuGet
  • Basic understanding of DICOM concepts from previous tutorials
  • You can find all the code demonstrated in this tutorial on GitHub here

“A picture is worth a thousand words, but the right window setting is worth a thousand diagnoses.” ~ Unknown Radiologist

The Theory Behind Presentation States

Medical images, particularly CT and MR, store raw acquisition data with wide dynamic ranges. A CT chest might have Hounsfield units from -1000 (air) to +3000 (dense bone), but a typical display can only show 256 gray levels. The radiologist must select which portion of that range to display - the "window." Different windows reveal different anatomy: lung window shows parenchyma, mediastinal window shows soft tissue, bone window shows skeletal detail. Presentation States separate this viewing intelligence from raw data.

The separation achieves a crucial goal: data integrity. The original image remains untouched while display preferences are stored separately. When a radiologist adjusts window/level and adds measurement annotations, these changes don't modify the acquisition. This means the original data is always available for re-interpretation, and the presentation state documents exactly how the image was viewed during diagnosis.

Graphic annotations present a philosophical challenge: should annotations be "burned in" to pixels or overlaid dynamically? Burned-in annotations permanently modify image data - simple but irreversible. Presentation State annotations overlay at display time, preserving the original. GSPS supports both graphic objects (arrows, circles, lines) and text labels, organized into layers that can be shown or hidden.

The grayscale pipeline in DICOM Part 14 defines the mathematical transformations from stored pixel values to displayed luminance: Modality LUT transformation (using either Rescale Slope/Intercept or a Modality LUT -- these are mutually exclusive alternatives for the same stage), VOI LUT (windowing), and Presentation LUT (display calibration). Presentation States can specify VOI LUT settings that slot into this pipeline, ensuring consistent appearance across different workstations calibrated to the Grayscale Standard Display Function.

For hanging protocols, Presentation States enable workflow automation. A protocol can specify that chest CT studies display with lung window in the main viewport, with a matching GSPS pre-applied. The radiologist sees images consistently prepared without manual adjustment, improving efficiency and reducing variability.

Presentation State Types

DICOM defines several types of Presentation States:

TypeSOP Class UIDUse Case
Grayscale Softcopy PS1.2.840.10008.5.1.4.1.1.11.1Most common; grayscale images
Color Softcopy PS1.2.840.10008.5.1.4.1.1.11.2Color images
Pseudo-Color Softcopy PS1.2.840.10008.5.1.4.1.1.11.3Apply color LUT to grayscale
Blending Softcopy PS1.2.840.10008.5.1.4.1.1.11.4Fusion (PET/CT)

Key Modules in Presentation States

  • Softcopy VOI LUT: Window Width/Level settings
  • Graphic Annotation: Text labels and graphic shapes
  • Graphic Layer: Layer organization for annotations
  • Display Shutter: Hide portions of the image
  • Displayed Area: Zoom and pan settings
  • Spatial Transformation: Rotation and flip

Step 1 of 3: Creating a Grayscale Presentation State

Here's how to create a GSPS with window settings:

using System;
using System.Diagnostics;
using System.IO;
using FellowOakDicom;

namespace DicomPresentationStates
{
    public class Program
    {
        private static readonly string OutputPath =
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Output");

        public static void Main(string[] args)
        {
            try
            {
                LogToDebugConsole("=== DICOM Presentation States Demo ===");

                if (!Directory.Exists(OutputPath))
                {
                    Directory.CreateDirectory(OutputPath);
                }

                CreateGrayscalePresentationState();
            }
            catch (Exception e)
            {
                LogToDebugConsole($"Error: {e.Message}");
            }
        }

        private static void CreateGrayscalePresentationState()
        {
            var dataset = new DicomDataset();

            string currentDate = DateTime.Now.ToString("yyyyMMdd");
            string currentTime = DateTime.Now.ToString("HHmmss");

            // Referenced image UIDs (use actual UIDs from your images)
            string referencedStudyUID = "1.2.3.4.5.6.7.8.9";
            string referencedSeriesUID = "1.2.3.4.5.6.7.8.9.1";
            string referencedSOPInstanceUID = "1.2.3.4.5.6.7.8.9.1.1";
            string referencedSOPClassUID = DicomUID.CTImageStorage.UID;

            //---------------------------------------------------------------
            // SOP Common Module
            //---------------------------------------------------------------
            dataset.Add(DicomTag.SOPClassUID,
                DicomUID.GrayscaleSoftcopyPresentationStateStorage);
            dataset.Add(DicomTag.SOPInstanceUID, DicomUID.Generate());

            //---------------------------------------------------------------
            // Patient Module (must match referenced image)
            //---------------------------------------------------------------
            dataset.Add(DicomTag.PatientName, "Doe^John");
            dataset.Add(DicomTag.PatientID, "PAT123");
            dataset.Add(DicomTag.PatientBirthDate, "19700101");
            dataset.Add(DicomTag.PatientSex, "M");

            //---------------------------------------------------------------
            // General Study Module (same as referenced study)
            //---------------------------------------------------------------
            dataset.Add(DicomTag.StudyInstanceUID, referencedStudyUID);
            dataset.Add(DicomTag.StudyDate, currentDate);
            dataset.Add(DicomTag.StudyTime, currentTime);
            dataset.Add(DicomTag.AccessionNumber, "ACC123");

            //---------------------------------------------------------------
            // Presentation State Module
            //---------------------------------------------------------------
            dataset.Add(DicomTag.Modality, "PR");  // Presentation State
            dataset.Add(DicomTag.SeriesInstanceUID, DicomUID.Generate());
            dataset.Add(DicomTag.SeriesNumber, "100");
            dataset.Add(DicomTag.InstanceNumber, "1");

            // User-friendly labels
            dataset.Add(DicomTag.ContentLabel, "CT_LUNG_WINDOW");
            dataset.Add(DicomTag.ContentDescription, "Lung window preset");
            dataset.Add(DicomTag.PresentationCreationDate, currentDate);
            dataset.Add(DicomTag.PresentationCreationTime, currentTime);
            dataset.Add(DicomTag.ContentCreatorName, "Tech^John");

            // Add referenced image
            AddReferencedImage(dataset, referencedSeriesUID,
                referencedSOPClassUID, referencedSOPInstanceUID);

            // Add VOI LUT (window settings)
            AddVoiLut(dataset, referencedSOPClassUID, referencedSOPInstanceUID,
                windowCenter: -600, windowWidth: 1500);  // Lung window

            // Save the file
            string outputFile = Path.Combine(OutputPath, "presentation_state.dcm");
            var dicomFile = new DicomFile(dataset);
            dicomFile.Save(outputFile);

            LogToDebugConsole($"Presentation State created: {outputFile}");
        }

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

Step 2 of 3: Adding Referenced Images and VOI LUT

The presentation state must reference the images it applies to:

private static void AddReferencedImage(DicomDataset dataset,
    string seriesUID, string sopClassUID, string sopInstanceUID)
{
    //---------------------------------------------------------------
    // Referenced Series Sequence
    //---------------------------------------------------------------
    var refSeriesSeq = new DicomSequence(DicomTag.ReferencedSeriesSequence);
    var refSeriesItem = new DicomDataset();
    refSeriesItem.Add(DicomTag.SeriesInstanceUID, seriesUID);

    // Referenced Image Sequence
    var refImageSeq = new DicomSequence(DicomTag.ReferencedImageSequence);
    var refImageItem = new DicomDataset();
    refImageItem.Add(DicomTag.ReferencedSOPClassUID, sopClassUID);
    refImageItem.Add(DicomTag.ReferencedSOPInstanceUID, sopInstanceUID);
    refImageSeq.Items.Add(refImageItem);
    refSeriesItem.Add(refImageSeq);

    refSeriesSeq.Items.Add(refSeriesItem);
    dataset.Add(refSeriesSeq);
}

private static void AddVoiLut(DicomDataset dataset,
    string sopClassUID, string sopInstanceUID,
    double windowCenter, double windowWidth)
{
    //---------------------------------------------------------------
    // Softcopy VOI LUT Module (Window Width/Level)
    //---------------------------------------------------------------
    var voiLutSeq = new DicomSequence(DicomTag.SoftcopyVOILUTSequence);
    var voiLutItem = new DicomDataset();

    // Reference the image this applies to
    var refImageSeq = new DicomSequence(DicomTag.ReferencedImageSequence);
    var refImageItem = new DicomDataset();
    refImageItem.Add(DicomTag.ReferencedSOPClassUID, sopClassUID);
    refImageItem.Add(DicomTag.ReferencedSOPInstanceUID, sopInstanceUID);
    refImageSeq.Items.Add(refImageItem);
    voiLutItem.Add(refImageSeq);

    // Window settings
    voiLutItem.Add(DicomTag.WindowCenter, windowCenter.ToString());
    voiLutItem.Add(DicomTag.WindowWidth, windowWidth.ToString());
    voiLutItem.Add(DicomTag.VOILUTFunction, "LINEAR");

    voiLutSeq.Items.Add(voiLutItem);
    dataset.Add(voiLutSeq);
}

Step 3 of 3: Adding Graphic Annotations

Presentation states can also include text and graphic annotations:

private static void AddGraphicLayer(DicomDataset dataset)
{
    //---------------------------------------------------------------
    // Graphic Layer Module
    //---------------------------------------------------------------
    var graphicLayerSeq = new DicomSequence(DicomTag.GraphicLayerSequence);
    var layerItem = new DicomDataset();
    layerItem.Add(DicomTag.GraphicLayer, "FINDINGS");
    layerItem.Add(DicomTag.GraphicLayerOrder, "1");
    layerItem.Add(DicomTag.GraphicLayerDescription, "Radiologist findings");
    graphicLayerSeq.Items.Add(layerItem);
    dataset.Add(graphicLayerSeq);
}

private static void AddTextAnnotation(DicomDataset dataset,
    string sopClassUID, string sopInstanceUID,
    string text, double[] boundingBox)
{
    //---------------------------------------------------------------
    // Graphic Annotation Module
    //---------------------------------------------------------------
    var annotationSeq = new DicomSequence(DicomTag.GraphicAnnotationSequence);
    var annotationItem = new DicomDataset();

    // Reference the image
    var refImageSeq = new DicomSequence(DicomTag.ReferencedImageSequence);
    var refImageItem = new DicomDataset();
    refImageItem.Add(DicomTag.ReferencedSOPClassUID, sopClassUID);
    refImageItem.Add(DicomTag.ReferencedSOPInstanceUID, sopInstanceUID);
    refImageSeq.Items.Add(refImageItem);
    annotationItem.Add(refImageSeq);

    // Layer assignment
    annotationItem.Add(DicomTag.GraphicLayer, "FINDINGS");

    // Text Object Sequence
    var textSeq = new DicomSequence(DicomTag.TextObjectSequence);
    var textItem = new DicomDataset();
    textItem.Add(DicomTag.BoundingBoxAnnotationUnits, "PIXEL");
    textItem.Add(DicomTag.UnformattedTextValue, text);
    textItem.Add(DicomTag.BoundingBoxTopLeftHandCorner, boundingBox[0], boundingBox[1]);
    textItem.Add(DicomTag.BoundingBoxBottomRightHandCorner, boundingBox[2], boundingBox[3]);
    textSeq.Items.Add(textItem);
    annotationItem.Add(textSeq);

    annotationSeq.Items.Add(annotationItem);
    dataset.Add(annotationSeq);
}

Common CT Window Presets

Here are standard window presets for CT imaging:

PresetWindow WidthWindow CenterUse Case
Lung1500-600Pulmonary parenchyma
Mediastinum35050Mediastinal structures
Soft Tissue40040General soft tissue
Bone2000300Skeletal structures
Brain8040Brain parenchyma
Subdural20075Subdural hematoma
Stroke4040Acute ischemia
Liver15030Hepatic lesions
Abdomen40050Abdominal organs

Reading Presentation States

To read and apply a presentation state:

public static void ReadPresentationState(string psFilePath)
{
    var file = DicomFile.Open(psFilePath);
    var dataset = file.Dataset;

    // Verify it's a Presentation State
    var sopClass = dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, "");
    LogToDebugConsole($"SOP Class: {sopClass}");
    LogToDebugConsole($"Modality: {dataset.GetSingleValueOrDefault(DicomTag.Modality, "")}");
    LogToDebugConsole($"Label: {dataset.GetSingleValueOrDefault(DicomTag.ContentLabel, "")}");

    // Read VOI LUT settings
    var voiLutSeq = dataset.GetSequence(DicomTag.SoftcopyVOILUTSequence);
    if (voiLutSeq != null && voiLutSeq.Items.Count > 0)
    {
        var voiItem = voiLutSeq.Items[0];
        var windowCenter = voiItem.GetSingleValueOrDefault(DicomTag.WindowCenter, "");
        var windowWidth = voiItem.GetSingleValueOrDefault(DicomTag.WindowWidth, "");
        LogToDebugConsole($"Window Center: {windowCenter}");
        LogToDebugConsole($"Window Width: {windowWidth}");
    }
}

Best Practices

  • Match patient data: PS must match the referenced image's patient info
  • Use meaningful labels: Content Label should identify the preset clearly
  • Document creator: Include who created the presentation state
  • One PS per purpose: Create separate states for different presets
  • Test compatibility: Verify your viewer supports the PS features used

Conclusion

Presentation States provide a powerful way to capture and share display settings separately from image data. This enables workflow improvements such as hanging protocols that automatically apply the right window settings, annotations that persist across viewing sessions, and standardized display presets across institutions.

Understanding Presentation States is essential for building diagnostic viewers and PACS integration. The most commonly used type is Grayscale Softcopy Presentation State (GSPS), which handles window/level settings and annotations for grayscale images like CT and MR.

Please check out the next tutorial in this series where we cover DICOM Key Object Selection documents.