This section covers the practical aspects of reading, writing, and manipulating DICOM files.
File Structure
A DICOM file has a well-defined structure:
Reading DICOM Files with Error Handling
Production DICOM applications must handle various error conditions including missing files, corrupted data, and incomplete DICOM objects. Implementing comprehensive error handling prevents crashes and provides meaningful feedback to users and logs. Always validate that critical attributes exist before processing.
Safe File Reading
public DicomImage SafeReadDicomFile(string filePath)
{
try
{
// Verify file exists
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"DICOM file not found: {filePath}");
}
// Read the DICOM file
DicomImage image = new DicomImage(filePath);
// Validate it's a valid DICOM file
if (!image.Attributes.Contains("00020000")) // File Meta Information Group Length
{
throw new InvalidDataException("File does not contain DICOM meta information");
}
// Verify required attributes
if (!image.Attributes.Contains("0020000D")) // Study Instance UID
{
throw new InvalidDataException("Missing required Study Instance UID");
}
if (!image.Attributes.Contains("00080016")) // SOP Class UID
{
throw new InvalidDataException("Missing required SOP Class UID");
}
return image;
}
catch (DicomException ex)
{
Console.WriteLine($"DICOM Error: {ex.Message}");
throw;
}
catch (Exception ex)
{
Console.WriteLine($"Error reading DICOM file: {ex.Message}");
throw;
}
}
Parsing File Meta Information
File Meta Information (Group 0002) contains essential data about the DICOM file itself, including the Transfer Syntax and SOP Class. This information is always encoded using Explicit VR Little Endian regardless of the dataset’s transfer syntax. Extracting this metadata helps determine how to process the file and verify its compatibility.
public void DisplayFileMetaInformation(DicomImage image)
{
Console.WriteLine("File Meta Information:");
// Transfer Syntax UID (0002,0010)
if (image.Attributes.Contains("00020010"))
{
string transferSyntax = image.Attributes["00020010"].Value.ToString();
Console.WriteLine($"Transfer Syntax: {GetTransferSyntaxName(transferSyntax)}");
}
// Media Storage SOP Class UID (0002,0002)
if (image.Attributes.Contains("00020002"))
{
string sopClass = image.Attributes["00020002"].Value.ToString();
Console.WriteLine($"SOP Class: {GetSOPClassName(sopClass)}");
}
// Media Storage SOP Instance UID (0002,0003)
if (image.Attributes.Contains("00020003"))
{
string sopInstance = image.Attributes["00020003"].Value.ToString();
Console.WriteLine($"SOP Instance: {sopInstance}");
}
// Implementation Class UID (0002,0012)
if (image.Attributes.Contains("00020012"))
{
string implClass = image.Attributes["00020012"].Value.ToString();
Console.WriteLine($"Implementation Class: {implClass}");
}
}
private string GetTransferSyntaxName(string uid)
{
return uid switch
{
"1.2.840.10008.1.2" => "Implicit VR Little Endian",
"1.2.840.10008.1.2.1" => "Explicit VR Little Endian",
"1.2.840.10008.1.2.2" => "Explicit VR Big Endian",
"1.2.840.10008.1.2.4.70" => "JPEG Lossless",
"1.2.840.10008.1.2.4.90" => "JPEG 2000 Lossless",
"1.2.840.10008.1.2.4.91" => "JPEG 2000 Lossy",
_ => "Unknown"
};
}
private string GetSOPClassName(string uid)
{
return uid switch
{
"1.2.840.10008.5.1.4.1.1.2" => "CT Image Storage",
"1.2.840.10008.5.1.4.1.1.4" => "MR Image Storage",
"1.2.840.10008.5.1.4.1.1.7" => "Secondary Capture Image Storage",
"1.2.840.10008.5.1.4.1.1.128" => "PET Image Storage",
_ => "Unknown SOP Class"
};
}
Working with Pixel Data
Pixel data handling is central to medical imaging applications. DICOM images can vary significantly in bit depth, color representation, and photometric interpretation. Understanding these attributes is essential for correctly rendering images and performing image processing operations.
Understanding Pixel Data Attributes
Before accessing pixel data, you must understand the image characteristics defined by these attributes. The combination of these values determines how to interpret the raw bytes and convert them to displayable images.
Extracting and Processing Pixel Data
The following code demonstrates extracting and processing pixel data while handling different bit depths and signed/unsigned representations. Window/level values should be applied for proper visualization on standard displays.
public void ExtractAndProcessPixelData(DicomImage image)
{
// Get image dimensions
int rows = Convert.ToInt32(image.Attributes["00280010"].Value);
int columns = Convert.ToInt32(image.Attributes["00280011"].Value);
// Get pixel characteristics
int bitsAllocated = Convert.ToInt32(image.Attributes["00280100"].Value);
int bitsStored = Convert.ToInt32(image.Attributes["00280101"].Value);
int highBit = Convert.ToInt32(image.Attributes["00280102"].Value);
int pixelRepresentation = Convert.ToInt32(image.Attributes["00280103"].Value);
// 0 = unsigned, 1 = signed
bool isSigned = pixelRepresentation == 1;
// Samples per pixel (1 = grayscale, 3 = RGB)
int samplesPerPixel = Convert.ToInt32(image.Attributes["00280002"].Value);
// Get photometric interpretation
string photometric = image.Attributes["00280004"].Value.ToString();
Console.WriteLine($"Image: {columns}x{rows}, {bitsAllocated}-bit, {photometric}");
// Extract pixel data
byte[] pixelData = image.PixelData;
// Process based on bits allocated
if (bitsAllocated == 8)
{
// 8-bit grayscale
ProcessPixels8Bit(pixelData, rows, columns);
}
else if (bitsAllocated == 16)
{
// 16-bit grayscale (common for CT/MR)
ProcessPixels16Bit(pixelData, rows, columns, isSigned);
}
// Apply window/level for display
if (image.Attributes.Contains("00281050") && image.Attributes.Contains("00281051"))
{
double windowCenter = Convert.ToDouble(image.Attributes["00281050"].Value);
double windowWidth = Convert.ToDouble(image.Attributes["00281051"].Value);
double minValue = windowCenter - (windowWidth / 2.0);
double maxValue = windowCenter + (windowWidth / 2.0);
Console.WriteLine($"Window: Center={windowCenter}, Width={windowWidth}");
}
}
private void ProcessPixels8Bit(byte[] pixelData, int rows, int columns)
{
for (int y = 0; y < rows; y++)
{
for (int x = 0; x < columns; x++)
{
byte pixelValue = pixelData[y * columns + x];
// Process pixel value
}
}
}
private void ProcessPixels16Bit(byte[] pixelData, int rows, int columns, bool isSigned)
{
for (int i = 0; i < pixelData.Length; i += 2)
{
if (isSigned)
{
short pixelValue = BitConverter.ToInt16(pixelData, i);
// Process signed pixel value
}
else
{
ushort pixelValue = BitConverter.ToUInt16(pixelData, i);
// Process unsigned pixel value
}
}
}
Modifying DICOM Files
Modifying DICOM files is common for anonymization, coercion of metadata, and preparing images for research. When anonymizing files for sharing or research, you must remove or replace all Protected Health Information (PHI) and generate new UIDs to prevent re-identification through linkage attacks.
Anonymization Example
The following example demonstrates comprehensive anonymization including PHI removal and UID regeneration. For HIPAA compliance and research use, follow the Safe Harbor or Expert Determination methods outlined in DICOM PS3.15 Annex E.
public void ModifyDicomFile(string inputPath, string outputPath)
{
using (DicomImage image = new DicomImage(inputPath))
{
// Anonymize patient information
image.Attributes["00100010"].Value = "ANONYMOUS"; // Patient Name
image.Attributes["00100020"].Value = "ANON" +
Guid.NewGuid().ToString("N").Substring(0, 8); // Patient ID
// Remove patient birth date
if (image.Attributes.Contains("00100030"))
{
image.Attributes.Remove("00100030");
}
// Update study description
image.Attributes["00081030"].Value = "ANONYMIZED STUDY";
// Add institution name
image.Attributes["00080080"].Value = "Research Institution";
// Update date to current date
image.Attributes["00080020"].Value = DateTime.Now.ToString("yyyyMMdd");
// Generate new UIDs to break linkage
image.Attributes["0020000D"].Value = DicomUID.Generate(); // Study Instance UID
image.Attributes["0020000E"].Value = DicomUID.Generate(); // Series Instance UID
image.Attributes["00080018"].Value = DicomUID.Generate(); // SOP Instance UID
// Update file meta information
image.Attributes["00020003"].Value = image.Attributes["00080018"].Value;
// Save modified file
image.Save(outputPath);
Console.WriteLine($"Anonymized file saved to: {outputPath}");
}
}
Attributes to Consider for Anonymization
Batch Processing DICOM Files
Processing large collections of DICOM files is common for archive migrations, quality control, and data extraction projects. Batch processing must handle files efficiently while gracefully managing errors without stopping the entire process. Asynchronous patterns help maximize throughput when I/O bound operations dominate.
public async Task ProcessDicomDirectory(
string directoryPath,
Func<DicomImage, Task> processor)
{
// Get all files (DICOM files may not have .dcm extension)
var files = Directory.GetFiles(directoryPath, "*.*", SearchOption.AllDirectories);
int processedCount = 0;
int errorCount = 0;
foreach (var file in files)
{
try
{
// Check if file is DICOM by reading first 132 bytes
if (!IsDicomFile(file))
continue;
using (DicomImage image = new DicomImage(file))
{
await processor(image);
processedCount++;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error processing {file}: {ex.Message}");
errorCount++;
}
}
Console.WriteLine($"Processed: {processedCount} files");
Console.WriteLine($"Errors: {errorCount} files");
}
private bool IsDicomFile(string filePath)
{
try
{
byte[] header = new byte[132];
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
if (fs.Length < 132) return false;
fs.Read(header, 0, 132);
}
// Check for "DICM" prefix at offset 128
return header[128] == 0x44 && // D
header[129] == 0x49 && // I
header[130] == 0x43 && // C
header[131] == 0x4D; // M
}
catch
{
return false;
}
}
Usage Example
// Process all DICOM files in a directory
await ProcessDicomDirectory(@"C:\DICOM\Studies", async (image) =>
{
// Extract patient info
string patientName = image.Attributes["00100010"].Value?.ToString() ?? "Unknown";
string studyDate = image.Attributes["00080020"].Value?.ToString() ?? "Unknown";
Console.WriteLine($"Processing: {patientName}, Study Date: {studyDate}");
// Perform any processing needed
await Task.CompletedTask;
});
Summary
Working with DICOM files involves:
- Understanding File Structure: Preamble, prefix, meta info, dataset
- Safe Reading: Validate file format and required attributes
- Pixel Data Handling: Account for bit depth, signedness, photometric interpretation
- Modification: Proper anonymization with UID regeneration
- Batch Processing: Efficient handling of large file sets
These techniques form the foundation for building robust DICOM applications.
Related Articles
Dive deeper into DICOM file operations with these tutorials:
Java DICOM Programming:
- DICOM Basics using Java - Making Sense of the DICOM File - File structure deep dive
- DICOM Basics using Java - Creating a DICOM File - Creating DICOM files programmatically
- DICOM Basics using Java - Extracting Image Data - Working with pixel data
- DICOM Basics using Java - Viewing DICOM Images - Image visualization
.NET DICOM Programming:
- DICOM Basics using .NET - Making Sense of the DICOM File - .NET file handling
- DICOM Basics using .NET - Creating a DICOM File - .NET DICOM creation
- DICOM Basics using .NET - Extracting Image Data - .NET pixel data extraction
- DICOM Basics using .NET - Viewing DICOM Images - .NET image display