DICOM Basics using .NET and C# - Creating a DICOM File

Introduction

This is part of my series of articles on the DICOM standard. In the previous tutorial, we explored how to make sense of the DICOM file and extract information from it. In this tutorial, we'll take the next step by learning how to create a DICOM file from scratch using .NET and the fo-dicom library.

Creating DICOM files programmatically is an essential skill when working with medical imaging applications. Whether you're building a secondary capture application, importing images from non-DICOM sources, or creating test data for your DICOM applications, understanding how to properly construct a DICOM file with all required attributes is fundamental.

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 tags and modules from the previous tutorial
  • You can find all the code demonstrated in this tutorial on GitHub here

“The creation of something new is not accomplished by the intellect but by the play instinct.” ~ Carl Jung

Key Concepts

Before we dive into the code, let's review some key concepts about DICOM file creation:

  • DICOM Modules: DICOM files must contain minimum required attributes organized into modules
  • Patient Module: Contains patient identification information (Patient Name, Patient ID)
  • Study Module: Contains study identification (Study Instance UID, Study ID, Study Date)
  • Series Module: Contains series identification (Series Instance UID, Series Number, Modality)
  • SOP Common Module: Contains SOP Class UID and SOP Instance UID
  • Secondary Capture: Used for images not directly acquired from a medical imaging device

Step 1 of 3: Setting Up the Project

First, create a new .NET console application and add the fo-dicom NuGet package. The basic project structure includes the necessary namespaces for DICOM file handling:

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

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

        public static void Main(string[] args)
        {
            try
            {
                LogToDebugConsole("=== Creating DICOM File Tutorial ===");

                // Ensure output directory exists
                if (!Directory.Exists(OutputPath))
                {
                    Directory.CreateDirectory(OutputPath);
                    LogToDebugConsole($"Created output directory: {OutputPath}");
                }

                // Create a new DICOM dataset
                var dataset = new DicomDataset();

                // Add required modules (shown in next steps)
                // ...

            }
            catch (Exception e)
            {
                LogToDebugConsole($"Error creating DICOM file: {e.Message}");
            }
        }

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

Step 2 of 3: Adding Required DICOM Modules

Now let's add all the required DICOM modules. Each module contains specific attributes that identify the patient, study, series, and image:

// Patient Module (Required)
// These attributes identify the patient
LogToDebugConsole("Adding Patient Module attributes...");
dataset.Add(DicomTag.PatientName, "Doe^John");
dataset.Add(DicomTag.PatientID, "PAT123456");
dataset.Add(DicomTag.PatientBirthDate, "19800101");
dataset.Add(DicomTag.PatientSex, "M");

// General Study Module (Required)
// These attributes describe the imaging study/exam
LogToDebugConsole("Adding Study Module attributes...");
var studyInstanceUid = DicomUID.Generate();
dataset.Add(DicomTag.StudyInstanceUID, studyInstanceUid);
dataset.Add(DicomTag.StudyID, "STUDY001");
dataset.Add(DicomTag.StudyDate, DateTime.Now.ToString("yyyyMMdd"));
dataset.Add(DicomTag.StudyTime, DateTime.Now.ToString("HHmmss"));
dataset.Add(DicomTag.AccessionNumber, "ACC123456");
dataset.Add(DicomTag.ReferringPhysicianName, "Smith^Jane^Dr");
dataset.Add(DicomTag.StudyDescription, "Sample Study Created by fo-dicom");

// General Series Module (Required)
// These attributes describe the series within the study
LogToDebugConsole("Adding Series Module attributes...");
var seriesInstanceUid = DicomUID.Generate();
dataset.Add(DicomTag.SeriesInstanceUID, seriesInstanceUid);
dataset.Add(DicomTag.SeriesNumber, "1");
dataset.Add(DicomTag.SeriesDate, DateTime.Now.ToString("yyyyMMdd"));
dataset.Add(DicomTag.SeriesTime, DateTime.Now.ToString("HHmmss"));
dataset.Add(DicomTag.Modality, "OT");  // OT = Other
dataset.Add(DicomTag.SeriesDescription, "Sample Series");

// General Image Module (Required)
LogToDebugConsole("Adding Image Module attributes...");
dataset.Add(DicomTag.InstanceNumber, "1");
dataset.Add(DicomTag.ContentDate, DateTime.Now.ToString("yyyyMMdd"));
dataset.Add(DicomTag.ContentTime, DateTime.Now.ToString("HHmmss"));

// SOP Common Module (Required)
// Secondary Capture SOP Class - used for images not from a medical device
LogToDebugConsole("Adding SOP Common Module attributes...");
dataset.Add(DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage);
var sopInstanceUid = DicomUID.Generate();
dataset.Add(DicomTag.SOPInstanceUID, sopInstanceUid);

Note how we use DicomUID.Generate() to create unique identifiers for the Study, Series, and SOP Instance UIDs. These UIDs must be globally unique to properly identify each DICOM object.

Step 3 of 3: Adding Pixel Data and Saving

The final step is to add the Image Pixel Module with pixel data and save the DICOM file:

// Image Pixel Module (Required for images with pixel data)
// For this example, we create a simple 256x256 grayscale image
LogToDebugConsole("Adding Image Pixel Module attributes...");
const int rows = 256;
const int columns = 256;

dataset.Add(DicomTag.Rows, (ushort)rows);
dataset.Add(DicomTag.Columns, (ushort)columns);
dataset.Add(DicomTag.BitsAllocated, (ushort)8);
dataset.Add(DicomTag.BitsStored, (ushort)8);
dataset.Add(DicomTag.HighBit, (ushort)7);
dataset.Add(DicomTag.PixelRepresentation, (ushort)0);  // 0 = unsigned
dataset.Add(DicomTag.SamplesPerPixel, (ushort)1);      // 1 = grayscale
dataset.Add(DicomTag.PhotometricInterpretation, "MONOCHROME2");

// Create a simple gradient pattern for pixel data
var pixelData = new byte[rows * columns];
for (int y = 0; y < rows; y++)
{
    for (int x = 0; x < columns; x++)
    {
        // Create a diagonal gradient pattern
        pixelData[y * columns + x] = (byte)((x + y) % 256);
    }
}

// Add the pixel data to the dataset
var pixelDataElement = new DicomOtherByte(DicomTag.PixelData, pixelData);
dataset.Add(pixelDataElement);

// Create and save the DICOM file
LogToDebugConsole("Creating DICOM file...");
var dicomFile = new DicomFile(dataset);
dicomFile.Save(OutputDicomFile);

LogToDebugConsole($"DICOM file created successfully!");
LogToDebugConsole($"Output file: {OutputDicomFile}");

// Display summary of created file
LogToDebugConsole("--- Created File Summary ---");
LogToDebugConsole($"  Patient Name:       {dataset.GetSingleValueOrDefault(DicomTag.PatientName, "")}");
LogToDebugConsole($"  Patient ID:         {dataset.GetSingleValueOrDefault(DicomTag.PatientID, "")}");
LogToDebugConsole($"  Study Instance UID: {studyInstanceUid}");
LogToDebugConsole($"  Series Instance UID: {seriesInstanceUid}");
LogToDebugConsole($"  SOP Instance UID:   {sopInstanceUid}");
LogToDebugConsole($"  Image Size:         {rows} x {columns} pixels");

The output of running this code will create a valid DICOM file with a gradient pattern that can be opened by any DICOM viewer:

=== Creating DICOM File Tutorial ===
Adding Patient Module attributes...
Adding Study Module attributes...
Adding Series Module attributes...
Adding Image Module attributes...
Adding SOP Common Module attributes...
Adding Image Pixel Module attributes...
Creating DICOM file...
DICOM file created successfully!
Output file: C:\...\Output\created_dicom_file.dcm
--- Created File Summary ---
  Patient Name:       Doe^John
  Patient ID:         PAT123456
  Study Instance UID: 1.2.826.0.1.3680043.2.1143.xxx
  Series Instance UID: 1.2.826.0.1.3680043.2.1143.xxx
  SOP Instance UID:   1.2.826.0.1.3680043.2.1143.xxx
  Image Size:         256 x 256 pixels

Understanding Key Image Pixel Attributes

When creating DICOM files with image data, understanding these pixel attributes is essential:

AttributeDescription
Rows/ColumnsImage dimensions in pixels
Bits AllocatedMemory bits per pixel (8, 16, or 32)
Bits StoredActual bits used for pixel values
High BitPosition of the most significant bit
Pixel Representation0 = unsigned, 1 = signed
Samples Per Pixel1 = grayscale, 3 = RGB color
Photometric InterpretationMONOCHROME1, MONOCHROME2, or RGB

Conclusion

In this tutorial, we've learned how to create a DICOM file from scratch using .NET and the fo-dicom library. We covered the essential DICOM modules (Patient, Study, Series, Image, and SOP Common), how to generate unique identifiers, and how to add pixel data to create a complete DICOM file.

This foundation is essential for building applications that need to convert non-DICOM images to DICOM format, create secondary capture images, or generate test data for DICOM workflows.

Please check out the next tutorial in this series where we cover extracting image data from DICOM files.