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:
| Attribute | Description |
|---|---|
| Rows/Columns | Image dimensions in pixels |
| Bits Allocated | Memory bits per pixel (8, 16, or 32) |
| Bits Stored | Actual bits used for pixel values |
| High Bit | Position of the most significant bit |
| Pixel Representation | 0 = unsigned, 1 = signed |
| Samples Per Pixel | 1 = grayscale, 3 = RGB color |
| Photometric Interpretation | MONOCHROME1, 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.