Working with DICOM Files

Section 9 of 27
33% complete

This section covers the practical aspects of reading, writing, and manipulating DICOM files.

File Structure

A DICOM file has a well-defined structure:

DICOM File Layout (Part 10)saravanansubramanian.coma DICOM file is a fixed header, a marker, then metadata followed by the datasetBYTE OFFSETSECTIONSIZE0 – 127start of fileFile Preambleusually all zerosmay hold application-specific data128 bytesfixed128 – 131magic markerDICOM PrefixASCII bytes D I C M — identifies file as DICOM Part 10DICM4 bytesfixed132 – Nmetadata blockFile Meta InformationGroup 0002 · always Explicit VR Little EndianTransfer Syntax UID (0002,0010) · SOP Class UID (0002,0002)SOP Instance UID (0002,0003) · Implementation infovariablelength-prefixedN+payloadDatasetencoded per Transfer Syntax declared in the meta infoPatient (0010)Study (0008, 0020) · Series (0008, 0020)Image (0028)Pixel Data (7FE0,0010)last element — may be encapsulated when compressedvariableto end of file

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.

Pixel Data Attributessaravanansubramanian.comthe tags you must read before decoding the raw pixel bytesGEOMETRYRows(0028,0010)image heightColumns(0028,0011)image widthSamples Per Pixel(0028,0002)1 = grayscale, 3 = RGBSTORAGE LAYOUTBits Allocated(0028,0100)storage bits (8 or 16)Bits Stored(0028,0101)actual bits usedHigh Bit(0028,0102)most significant bit positionPixel Representation(0028,0103)0 = unsigned, 1 = signedPHOTOMETRIC INTERPRETATION (0028,0004)MONOCHROME1low pixel values = whiteMONOCHROME2low pixel values = black (CT / MR default)RGBthree samples per pixel - color imageother values you may encounter: YBR_FULL, YBR_FULL_422, PALETTE COLOR, HSV

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

DICOM Attributes Containing PHIsaravanansubramanian.comevery tag on this map is a candidate for removal or replacement during de-identification!PROTECTED HEALTH INFORMATION - HIPAA / GDPR obligations applyPATIENT MODULEPatient Name(0010,0010)Patient ID(0010,0020)Patient Birth Date(0010,0030)Patient Sex(0010,0040)Patient Age(0010,1010)Patient Address(0010,1040)STUDY MODULEAccession Number(0008,0050)Referring Physician(0008,0090)Study Date(0008,0020)Study Time(0008,0030)EQUIPMENTInstitution Name(0008,0080)Station Name(0008,1010)Device Serial Number(0018,1000)UIDs - REQUIRED FOR COMPLETE DE-IDENTIFICATIONStudy Instance UID(0020,000D)Series Instance UID(0020,000E)SOP Instance UID(0008,0018)Anonymization tipReplace UIDs with freshly generated values - do not just blank them, or DICOM referential integrity breaks across Series and Study.

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:

  1. Understanding File Structure: Preamble, prefix, meta info, dataset
  2. Safe Reading: Validate file format and required attributes
  3. Pixel Data Handling: Account for bit depth, signedness, photometric interpretation
  4. Modification: Proper anonymization with UID regeneration
  5. Batch Processing: Efficient handling of large file sets

These techniques form the foundation for building robust DICOM applications.

Dive deeper into DICOM file operations with these tutorials:

Java DICOM Programming:

.NET DICOM Programming:

Quiz: Working with DICOM Files

Question 1 of 4

What are the first 132 bytes of a DICOM file?