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:
- A .NET development environment (Visual Studio or Visual Studio Code)
- The Fellow Oak DICOM library (fo-dicom) installed via NuGet
- Sample DICOM CT images for testing (available from dicomlibrary.com)
- You can find all the code demonstrated in this tutorial on GitHub here
“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:
| Preset | Width | Center | Use Case |
|---|---|---|---|
| Lung | 1500 | -600 | CT lung parenchyma |
| Bone | 2500 | 480 | CT bone structures |
| Soft Tissue | 400 | 40 | CT soft tissue |
| Brain | 80 | 40 | CT brain |
| Abdomen | 350 | 50 | CT abdomen |
| Mediastinum | 500 | 50 | CT mediastinum |
| Liver | 150 | 30 | CT 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:
| Feature | Description |
|---|---|
| Real-time Rendering | Image updates instantly as you adjust sliders |
| Preset Buttons | Quick access to common CT window settings |
| Keyboard Shortcuts | R=Reset, S=Save, 1-5=Presets, Esc=Close |
| DICOM Metadata Display | Shows patient, study, modality, and image info |
| Multi-format Export | Save views as PNG, JPEG, or BMP |
| Dark Theme UI | Professional 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.