DICOM Basics using .NET and C# - Viewing DICOM Images

Introduction

This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore how to properly view, display, manipulate, and save DICOM images by applying Window Width and Window Center (also known as Window Level) adjustments. These settings are crucial for visualizing different tissue types in medical images, especially CT and MR scans.

Understanding window/level adjustments is essential for anyone working with medical imaging applications. The same CT scan can appear dramatically different depending on the window settings used, allowing radiologists to visualize bone, soft tissue, or lung tissue from a single image. We'll also build an interactive Windows Forms viewer that allows real-time adjustment of these settings.

Prerequisites

Before you begin, ensure you have the following:

“Beauty is not in the face; beauty is a light in the heart.” ~ Kahlil Gibran

Understanding Window Width and Window Center

DICOM images, especially CT scans, contain pixel values that span a wide range (e.g., -1024 to +3071 Hounsfield Units for CT). However, computer displays can only show a limited range of gray values (typically 256). Window/Level settings map a portion of the full range to the displayable gray scale:

  • Window Width (WW): Controls contrast - the range of pixel values mapped to the display
  • Window Center/Level (WL): Controls brightness - the center point of the window

The formula for calculating displayed values is:

  • If pixel value <= WL - WW/2 → Display as black
  • If pixel value >= WL + WW/2 → Display as white
  • Values between are linearly mapped to gray levels

Common Window/Level Presets

Different tissue types require different window settings for optimal visualization:

PresetWidthCenterUse Case
Lung1500-600CT lung parenchyma
Bone2500480CT bone structures
Soft Tissue40040CT soft tissue
Brain8040CT brain
Abdomen35050CT abdomen
Mediastinum50050CT mediastinum
Liver15030CT liver

Step 1 of 5: Reading Window/Level Values from DICOM Files

First, let's set up our project and read the existing window/level values from a DICOM file:

//-----------------------------------------------------------------------
// Tutorial: Viewing DICOM Images with Window/Level Control
//-----------------------------------------------------------------------
// Purpose:
//   Demonstrates how to display DICOM images and apply Window Width
//   and Window Center (Level) adjustments for optimal visualization.
//   This tutorial includes an interactive Windows Forms viewer that
//   allows real-time adjustment of Window/Level settings.
//
// Key Concepts:
//   - Window Width (WW): Controls contrast - range of gray values displayed
//   - Window Center/Level (WL): Controls brightness - center of the range
//   - Formula: if (pixel <= WL - WW/2) => black; if (pixel >= WL + WW/2) => white
//   - Common CT Presets: Lung (WW=1500, WL=-600), Bone (WW=2500, WL=480)
//-----------------------------------------------------------------------

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

namespace Com.SaravananSubramanian.ViewingDicomImages
{
    public class Program
    {
        //-----------------------------------------------------------------------
        // Configuration: Path to DICOM test file
        // NOTE: Ensure a valid DICOM image file exists at this path before running
        // For best results, use a CT image with embedded window/level values
        //-----------------------------------------------------------------------
        private static readonly string PathToDicomTestFile =
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test Files", "CT_small.dcm");
        private static readonly string OutputPath =
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Output");

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

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

                // Verify the DICOM file exists
                if (!File.Exists(PathToDicomTestFile))
                {
                    LogToDebugConsole($"ERROR: DICOM file not found at: {PathToDicomTestFile}");
                    LogToDebugConsole("Please place a DICOM file in the 'Test Files' folder.");
                    return;
                }

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

                //-----------------------------------------------------------------------
                // Display existing Window/Level values from the DICOM file
                //-----------------------------------------------------------------------
                LogToDebugConsole("--- Window/Level Information ---");

                var windowCenter = dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, double.NaN);
                var windowWidth = dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, double.NaN);
                var windowExplanation = dataset.GetSingleValueOrDefault(DicomTag.WindowCenterWidthExplanation, "");

                if (!double.IsNaN(windowCenter) && !double.IsNaN(windowWidth))
                {
                    LogToDebugConsole($"  Original Window Center: {windowCenter}");
                    LogToDebugConsole($"  Original Window Width:  {windowWidth}");
                    if (!string.IsNullOrEmpty(windowExplanation))
                    {
                        LogToDebugConsole($"  Explanation: {windowExplanation}");
                    }
                }
                else
                {
                    LogToDebugConsole("  No window/level values embedded in file");
                }
                LogToDebugConsole("");

                // Continue with rendering and viewer launch...
            }
            catch (Exception e)
            {
                LogToDebugConsole($"Error viewing DICOM image: {e.Message}");
                LogToDebugConsole($"Stack trace: {e.StackTrace}");
            }
        }

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

Step 2 of 5: Rendering with Different Window Presets

Now let's render the same image with different window/level presets to see how they affect visualization. Add this code to the Main method:

//-----------------------------------------------------------------------
// Render with default settings and save to files
//-----------------------------------------------------------------------
LogToDebugConsole("--- Rendering Images to Files ---");

var dicomImage = new DicomImage(PathToDicomTestFile);

// Render with default (original) window/level
LogToDebugConsole("  Rendering with default settings...");
SaveRenderedImage(dicomImage, "default");

//-----------------------------------------------------------------------
// Demonstrate different Window/Level presets
// These presets are commonly used for CT images
//-----------------------------------------------------------------------

// Lung preset (typical for viewing lung parenchyma in CT)
LogToDebugConsole("  Rendering with Lung preset (WW=1500, WL=-600)...");
dicomImage.WindowWidth = 1500;
dicomImage.WindowCenter = -600;
SaveRenderedImage(dicomImage, "lung");

// Bone preset (typical for viewing bone in CT)
LogToDebugConsole("  Rendering with Bone preset (WW=2500, WL=480)...");
dicomImage.WindowWidth = 2500;
dicomImage.WindowCenter = 480;
SaveRenderedImage(dicomImage, "bone");

// Soft tissue preset (typical for viewing soft tissue in CT)
LogToDebugConsole("  Rendering with Soft Tissue preset (WW=400, WL=40)...");
dicomImage.WindowWidth = 400;
dicomImage.WindowCenter = 40;
SaveRenderedImage(dicomImage, "soft_tissue");

// Brain preset (typical for brain CT)
LogToDebugConsole("  Rendering with Brain preset (WW=80, WL=40)...");
dicomImage.WindowWidth = 80;
dicomImage.WindowCenter = 40;
SaveRenderedImage(dicomImage, "brain");

LogToDebugConsole("");
LogToDebugConsole($"All images saved to: {OutputPath}");

Step 3 of 5: Saving Rendered Images

Here's the helper method to save rendered images with different preset names:

/// <summary>
/// Renders the DICOM image and saves it with the specified preset name.
/// </summary>
private static void SaveRenderedImage(DicomImage dicomImage, string presetName)
{
    var outputPath = Path.Combine(OutputPath, $"view_{presetName}.png");
    var renderedImage = dicomImage.RenderImage(0);
    var bitmap = renderedImage.As<Bitmap>();
    bitmap.Save(outputPath, ImageFormat.Png);
    bitmap.Dispose();
}

The output will show the window/level information and confirm the rendering:

=== DICOM Image Viewing Tutorial ===
Reading DICOM file: C:\...\Test Files\CT_small.dcm

--- Window/Level Information ---
  Original Window Center: 40
  Original Window Width:  400
  Explanation: SOFT_TISSUE

--- Rendering Images to Files ---
  Rendering with default settings...
  Rendering with Lung preset (WW=1500, WL=-600)...
  Rendering with Bone preset (WW=2500, WL=480)...
  Rendering with Soft Tissue preset (WW=400, WL=40)...
  Rendering with Brain preset (WW=80, WL=40)...

All images saved to: C:\...\Output

Step 4 of 5: Building an Interactive DICOM Image Viewer

Now let's build an interactive Windows Forms viewer that allows real-time manipulation of Window/Level settings. This viewer includes slider controls, preset buttons, and keyboard shortcuts for efficient image viewing.

First, add the code to launch the viewer in your Main method:

//-----------------------------------------------------------------------
// Launch the Interactive DICOM Image Viewer
//-----------------------------------------------------------------------
LogToDebugConsole("");
LogToDebugConsole("--- Launching Interactive DICOM Viewer ---");
LogToDebugConsole("  Keyboard shortcuts:");
LogToDebugConsole("    R     - Reset to original window/level values");
LogToDebugConsole("    S     - Save current view to file");
LogToDebugConsole("    1-5   - Apply presets (1=Lung, 2=Bone, 3=Soft Tissue, 4=Brain, 5=Abdomen)");
LogToDebugConsole("    Esc   - Close viewer");
LogToDebugConsole("");

// Initialize Windows Forms
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

// Launch the interactive viewer
using (var viewer = new DicomImageViewer(PathToDicomTestFile))
{
    Application.Run(viewer);
}

LogToDebugConsole("Image viewing tutorial completed successfully!");

Now create the DicomImageViewer class. This Windows Forms viewer demonstrates how to display DICOM images with interactive Window Width and Window Center (Level) adjustments:

//-----------------------------------------------------------------------
// DICOM Image Viewer - Interactive Window/Level Control
//-----------------------------------------------------------------------
// Key Features:
//   - Real-time image rendering as W/L values change
//   - Preset buttons for common CT viewing configurations
//   - TrackBar controls for fine adjustment
//   - Keyboard shortcuts for efficient navigation
//   - Display of DICOM metadata (patient, study, modality, etc.)
//-----------------------------------------------------------------------

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Forms;
using FellowOakDicom;
using FellowOakDicom.Imaging;

namespace Com.SaravananSubramanian.ViewingDicomImages
{
    /// <summary>
    /// Interactive DICOM image viewer with Window/Level controls.
    /// </summary>
    public class DicomImageViewer : Form
    {
        // DICOM image object for rendering
        private DicomImage _dicomImage;
        private string _filePath;

        // UI Controls
        private PictureBox _pictureBox;
        private TrackBar _windowWidthSlider;
        private TrackBar _windowCenterSlider;
        private Label _windowWidthLabel;
        private Label _windowCenterLabel;
        private Label _windowWidthValueLabel;
        private Label _windowCenterValueLabel;
        private Panel _controlPanel;
        private FlowLayoutPanel _presetPanel;
        private Label _imageInfoLabel;
        private StatusStrip _statusStrip;
        private ToolStripStatusLabel _statusLabel;

        // Window/Level range constants (suitable for CT images)
        private const int MinWindowWidth = 1;
        private const int MaxWindowWidth = 4096;
        private const int MinWindowCenter = -2048;
        private const int MaxWindowCenter = 2048;

        // Common CT presets: Name, Width, Center
        private readonly (string Name, int Width, int Center)[] _presets = new[]
        {
            ("Default", 0, 0),           // Will use original values
            ("Lung", 1500, -600),
            ("Bone", 2500, 480),
            ("Soft Tissue", 400, 40),
            ("Brain", 80, 40),
            ("Abdomen", 350, 50),
            ("Mediastinum", 500, 50),
            ("Liver", 150, 30)
        };

        // Original W/L values from DICOM file
        private double _originalWindowWidth;
        private double _originalWindowCenter;

        /// <summary>
        /// Creates a new DICOM image viewer for the specified file.
        /// </summary>
        /// <param name="dicomFilePath">Path to the DICOM file to view</param>
        public DicomImageViewer(string dicomFilePath)
        {
            _filePath = dicomFilePath;
            InitializeComponent();
            LoadDicomImage();
        }

Next, implement the UI initialization method that creates all the controls programmatically:

/// <summary>
/// Initialize all UI components programmatically.
/// </summary>
private void InitializeComponent()
{
    // Form settings
    this.Text = "DICOM Image Viewer - Window/Level Demo";
    this.Size = new Size(1024, 768);
    this.MinimumSize = new Size(800, 600);
    this.StartPosition = FormStartPosition.CenterScreen;
    this.BackColor = Color.FromArgb(45, 45, 48);

    // Create main layout
    var mainContainer = new TableLayoutPanel
    {
        Dock = DockStyle.Fill,
        ColumnCount = 1,
        RowCount = 3
    };
    mainContainer.RowStyles.Add(new RowStyle(SizeType.Absolute, 100)); // Control panel
    mainContainer.RowStyles.Add(new RowStyle(SizeType.Percent, 100));  // Image area
    mainContainer.RowStyles.Add(new RowStyle(SizeType.Absolute, 25));  // Status bar

    // Control Panel
    _controlPanel = new Panel
    {
        Dock = DockStyle.Fill,
        BackColor = Color.FromArgb(60, 60, 65),
        Padding = new Padding(10)
    };

    // Window Width controls
    _windowWidthLabel = new Label
    {
        Text = "Window Width:",
        ForeColor = Color.White,
        Location = new Point(10, 10),
        AutoSize = true
    };

    _windowWidthSlider = new TrackBar
    {
        Minimum = MinWindowWidth,
        Maximum = MaxWindowWidth,
        Value = 400,
        TickFrequency = 200,
        LargeChange = 100,
        SmallChange = 10,
        Location = new Point(120, 5),
        Width = 300
    };
    _windowWidthSlider.ValueChanged += OnWindowWidthChanged;

    _windowWidthValueLabel = new Label
    {
        Text = "400",
        ForeColor = Color.LightGreen,
        Font = new Font("Consolas", 10, FontStyle.Bold),
        Location = new Point(430, 10),
        AutoSize = true
    };

    // Window Center controls
    _windowCenterLabel = new Label
    {
        Text = "Window Center:",
        ForeColor = Color.White,
        Location = new Point(10, 45),
        AutoSize = true
    };

    _windowCenterSlider = new TrackBar
    {
        Minimum = MinWindowCenter,
        Maximum = MaxWindowCenter,
        Value = 40,
        TickFrequency = 200,
        LargeChange = 100,
        SmallChange = 10,
        Location = new Point(120, 40),
        Width = 300
    };
    _windowCenterSlider.ValueChanged += OnWindowCenterChanged;

    _windowCenterValueLabel = new Label
    {
        Text = "40",
        ForeColor = Color.LightGreen,
        Font = new Font("Consolas", 10, FontStyle.Bold),
        Location = new Point(430, 45),
        AutoSize = true
    };

    // Preset buttons panel
    _presetPanel = new FlowLayoutPanel
    {
        Location = new Point(500, 5),
        Size = new Size(500, 85),
        FlowDirection = FlowDirection.LeftToRight,
        WrapContents = true
    };

    // Create preset buttons
    foreach (var preset in _presets)
    {
        var button = new Button
        {
            Text = preset.Name,
            Size = new Size(90, 35),
            Margin = new Padding(3),
            BackColor = Color.FromArgb(80, 80, 85),
            ForeColor = Color.White,
            FlatStyle = FlatStyle.Flat,
            Tag = preset
        };
        button.FlatAppearance.BorderColor = Color.Gray;
        button.Click += OnPresetButtonClick;
        _presetPanel.Controls.Add(button);
    }

    // Add controls to control panel
    _controlPanel.Controls.Add(_windowWidthLabel);
    _controlPanel.Controls.Add(_windowWidthSlider);
    _controlPanel.Controls.Add(_windowWidthValueLabel);
    _controlPanel.Controls.Add(_windowCenterLabel);
    _controlPanel.Controls.Add(_windowCenterSlider);
    _controlPanel.Controls.Add(_windowCenterValueLabel);
    _controlPanel.Controls.Add(_presetPanel);

    // Image display area
    var imagePanel = new Panel
    {
        Dock = DockStyle.Fill,
        BackColor = Color.Black,
        AutoScroll = true
    };

    _pictureBox = new PictureBox
    {
        BackColor = Color.Black,
        SizeMode = PictureBoxSizeMode.Zoom,
        Dock = DockStyle.Fill
    };
    imagePanel.Controls.Add(_pictureBox);

    // Image info label (overlay)
    _imageInfoLabel = new Label
    {
        AutoSize = true,
        BackColor = Color.FromArgb(150, 0, 0, 0),
        ForeColor = Color.Yellow,
        Font = new Font("Consolas", 9),
        Location = new Point(10, 10),
        Padding = new Padding(5)
    };
    _pictureBox.Controls.Add(_imageInfoLabel);

    // Status strip
    _statusStrip = new StatusStrip
    {
        BackColor = Color.FromArgb(0, 122, 204)
    };
    _statusLabel = new ToolStripStatusLabel
    {
        ForeColor = Color.White,
        Text = "Ready"
    };
    _statusStrip.Items.Add(_statusLabel);

    // Add to main container
    mainContainer.Controls.Add(_controlPanel, 0, 0);
    mainContainer.Controls.Add(imagePanel, 0, 1);
    mainContainer.Controls.Add(_statusStrip, 0, 2);

    this.Controls.Add(mainContainer);

    // Form events
    this.KeyPreview = true;
    this.KeyDown += OnKeyDown;
}

Now implement the DICOM loading and rendering methods:

/// <summary>
/// Loads the DICOM image and extracts metadata.
/// </summary>
private void LoadDicomImage()
{
    try
    {
        _statusLabel.Text = "Loading DICOM file...";

        // Load DICOM file and extract metadata
        var file = DicomFile.Open(_filePath);
        var dataset = file.Dataset;

        // Get original window/level values
        _originalWindowWidth = dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, 400.0);
        _originalWindowCenter = dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, 40.0);

        // Update presets array with original values
        _presets[0] = ("Default", (int)_originalWindowWidth, (int)_originalWindowCenter);

        // Extract image info for display
        var patientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, "Unknown");
        var studyDate = dataset.GetSingleValueOrDefault(DicomTag.StudyDate, "Unknown");
        var modality = dataset.GetSingleValueOrDefault(DicomTag.Modality, "Unknown");
        var rows = dataset.GetSingleValueOrDefault(DicomTag.Rows, 0);
        var columns = dataset.GetSingleValueOrDefault(DicomTag.Columns, 0);
        var bitsAllocated = dataset.GetSingleValueOrDefault(DicomTag.BitsAllocated, 0);
        var photometric = dataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, "Unknown");

        _imageInfoLabel.Text =
            $"Patient: {patientName}\n" +
            $"Study Date: {studyDate}\n" +
            $"Modality: {modality}\n" +
            $"Size: {columns} x {rows}\n" +
            $"Bits: {bitsAllocated}\n" +
            $"Photometric: {photometric}";

        // Create DICOM image for rendering
        _dicomImage = new DicomImage(_filePath);

        // Set initial slider values from file
        _windowWidthSlider.Value = ClampValue((int)_originalWindowWidth, MinWindowWidth, MaxWindowWidth);
        _windowCenterSlider.Value = ClampValue((int)_originalWindowCenter, MinWindowCenter, MaxWindowCenter);

        // Render the initial image
        RenderImage();

        _statusLabel.Text = $"Loaded: {Path.GetFileName(_filePath)} | Original W/L: {_originalWindowWidth}/{_originalWindowCenter}";
    }
    catch (Exception ex)
    {
        MessageBox.Show(
            $"Error loading DICOM file:\n{ex.Message}",
            "Load Error",
            MessageBoxButtons.OK,
            MessageBoxIcon.Error);
        _statusLabel.Text = "Error loading file";
    }
}

/// <summary>
/// Renders the DICOM image with current Window/Level settings.
/// </summary>
private void RenderImage()
{
    if (_dicomImage == null) return;

    try
    {
        // Apply current window/level settings
        _dicomImage.WindowWidth = _windowWidthSlider.Value;
        _dicomImage.WindowCenter = _windowCenterSlider.Value;

        // Render and display
        var renderedImage = _dicomImage.RenderImage(0);
        var bitmap = renderedImage.As<Bitmap>();

        // Dispose old image if exists
        _pictureBox.Image?.Dispose();
        _pictureBox.Image = bitmap;
    }
    catch (Exception ex)
    {
        _statusLabel.Text = $"Render error: {ex.Message}";
    }
}

Add the event handlers for slider changes and preset buttons:

/// <summary>
/// Handles Window Width slider changes.
/// </summary>
private void OnWindowWidthChanged(object sender, EventArgs e)
{
    _windowWidthValueLabel.Text = _windowWidthSlider.Value.ToString();
    RenderImage();
}

/// <summary>
/// Handles Window Center slider changes.
/// </summary>
private void OnWindowCenterChanged(object sender, EventArgs e)
{
    _windowCenterValueLabel.Text = _windowCenterSlider.Value.ToString();
    RenderImage();
}

/// <summary>
/// Handles preset button clicks.
/// </summary>
private void OnPresetButtonClick(object sender, EventArgs e)
{
    var button = (Button)sender;
    var preset = ((string Name, int Width, int Center))button.Tag;

    // Update sliders (which will trigger re-render)
    _windowWidthSlider.Value = ClampValue(preset.Width, MinWindowWidth, MaxWindowWidth);
    _windowCenterSlider.Value = ClampValue(preset.Center, MinWindowCenter, MaxWindowCenter);

    _statusLabel.Text = $"Applied preset: {preset.Name} (WW={preset.Width}, WL={preset.Center})";
}

/// <summary>
/// Applies a preset by index.
/// </summary>
private void ApplyPreset(int index)
{
    if (index >= 0 && index < _presets.Length)
    {
        var preset = _presets[index];
        _windowWidthSlider.Value = ClampValue(preset.Width, MinWindowWidth, MaxWindowWidth);
        _windowCenterSlider.Value = ClampValue(preset.Center, MinWindowCenter, MaxWindowCenter);
        _statusLabel.Text = $"Applied preset: {preset.Name}";
    }
}

Step 5 of 5: Implementing Keyboard Shortcuts and Saving Images

Finally, add keyboard shortcut support and the ability to save images in multiple formats:

/// <summary>
/// Handles keyboard shortcuts.
/// </summary>
private void OnKeyDown(object sender, KeyEventArgs e)
{
    switch (e.KeyCode)
    {
        case Keys.R: // Reset to default
            _windowWidthSlider.Value = ClampValue((int)_originalWindowWidth, MinWindowWidth, MaxWindowWidth);
            _windowCenterSlider.Value = ClampValue((int)_originalWindowCenter, MinWindowCenter, MaxWindowCenter);
            _statusLabel.Text = "Reset to original window/level values";
            break;

        case Keys.S: // Save current view
            SaveCurrentView();
            break;

        case Keys.Escape: // Close
            this.Close();
            break;

        case Keys.D1: // Preset 1 - Lung
            ApplyPreset(1);
            break;

        case Keys.D2: // Preset 2 - Bone
            ApplyPreset(2);
            break;

        case Keys.D3: // Preset 3 - Soft Tissue
            ApplyPreset(3);
            break;

        case Keys.D4: // Preset 4 - Brain
            ApplyPreset(4);
            break;

        case Keys.D5: // Preset 5 - Abdomen
            ApplyPreset(5);
            break;
    }
}

/// <summary>
/// Saves the current view as an image file (PNG, JPEG, or BMP).
/// </summary>
private void SaveCurrentView()
{
    try
    {
        var saveDialog = new SaveFileDialog
        {
            Filter = "PNG Image|*.png|JPEG Image|*.jpg|BMP Image|*.bmp",
            Title = "Save Current View",
            FileName = $"dicom_view_WW{_windowWidthSlider.Value}_WL{_windowCenterSlider.Value}"
        };

        if (saveDialog.ShowDialog() == DialogResult.OK)
        {
            var format = ImageFormat.Png;
            if (saveDialog.FileName.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase))
                format = ImageFormat.Jpeg;
            else if (saveDialog.FileName.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase))
                format = ImageFormat.Bmp;

            _pictureBox.Image.Save(saveDialog.FileName, format);
            _statusLabel.Text = $"Saved: {saveDialog.FileName}";
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Error saving image: {ex.Message}", "Save Error",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

/// <summary>
/// Clamps a value to the specified range.
/// </summary>
private static int ClampValue(int value, int min, int max)
{
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

/// <summary>
/// Clean up resources.
/// </summary>
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        _pictureBox?.Image?.Dispose();
    }
    base.Dispose(disposing);
}

Visual Comparison of Window Presets

When viewing a CT chest scan with different presets:

  • Lung Window: Clearly shows lung tissue, airways, and lung lesions. Everything else appears very bright.
  • Bone Window: Highlights bone structures and calcifications. Soft tissue appears washed out.
  • Soft Tissue Window: Best for viewing organs, muscles, and soft tissue masses.
  • Mediastinum Window: Optimized for viewing structures between the lungs.

Interactive Viewer Features Summary

The interactive DICOM viewer we built includes these key features:

FeatureDescription
Real-time RenderingImage updates instantly as you adjust sliders
Preset ButtonsQuick access to common CT window settings
Keyboard ShortcutsR=Reset, S=Save, 1-5=Presets, Esc=Close
DICOM Metadata DisplayShows patient, study, modality, and image info
Multi-format ExportSave views as PNG, JPEG, or BMP
Dark Theme UIProfessional dark interface reduces eye strain

Best Practices for Medical Image Display

When building medical imaging applications:

  • Always respect the embedded window/level values as the default view
  • Provide preset buttons for common window settings based on modality
  • Allow users to manually adjust window/level with mouse interactions or sliders
  • Display current window/level values so users know their settings
  • Implement keyboard shortcuts for power users
  • Support saving rendered images in common formats (PNG, JPEG, BMP)
  • Consider storing user preferences for window settings
  • For diagnostic reading, use calibrated medical displays
  • Always dispose of image resources properly to prevent memory leaks

Conclusion

In this tutorial, we've learned how to properly display, manipulate, and save DICOM images using window width and window center adjustments. We explored common presets for different tissue types and built an interactive Windows Forms viewer that allows real-time manipulation of these settings.

Understanding window/level is fundamental to medical image visualization. The same image data can reveal completely different anatomical structures depending on the window settings applied. In your medical imaging applications, always provide users with the ability to adjust these settings for optimal visualization, and remember to implement features for saving rendered images so users can export their work.

Please check out the next tutorial in this series where we cover how to read a DICOM Directory (DICOMDIR) file.