DICOM Basics using .NET and C# - Making Sense of the DICOM File

This is part of my series of articles on the DICOM standard. Before we get started on this tutorial, have a quick look at my earlier article titled “Introduction to the DICOM Standard” for a short and quick introduction to the standard. Please note that this tutorial assumes that you know C# (or any equivalent object-oriented language such as Java or C++).

Introduction

I thought I will start this programming tutorial series on DICOM with an very high-level introduction to the DICOM file format. Please keep the official DICOM standard document at this link handy as many things I cover in this tutorial are explained in more detail there.

As you recall from my introductory DICOM tutorial, a DICOM file often contains image data and data about the patient in the same file using the concept of tags (to be explained later) like other image formats such as TIFF. However, the information stored in the DICOM file is far more structured and diverse than other standards in the form of a DICOM Dictionary. This dictionary (see page 23 of this document) to get an idea how comprehensive it is) which contains several thousand of these tags helps us encode information about when and where the image was taken, information about the patient, the physician as well as diagnosis information along with image data. This minimizes the risk of attributing some critical healthcare information to a wrong patient.

Most DICOM files (sometimes with a .dcm extension) contain image data, and may sometimes even contain multiple images (or “frames” as they are often referred to) to enable what is called a cine-loop which allows a DICOM viewer to visualize the entire sequence of images as a movie. However, DICOM files don’t necessarily have to be about images as most people assume but are also used to store other information such as reports, ECG signals and even audio (which we will cover later).

“Education is not preparation for life; education is life itself” ~ John Dewey

In the sections that follow below, we will first understand the basic structure (and syntax) of the DICOM file which permits applications that may be running on different operating systems and devices to exchange image and image-related information with one another easily. We will then use a freely available but extremely powerful DICOM toolkit called fo-dicom DICOM toolkit to parse an actual DICOM file and see the content inside it relating back to the concepts we covered to help reinforce our understanding of the concepts covered. Please note that there is far more to the DICOM file than what I cover here, but for most developers like me who deal with processing of DICOM files using a toolkit or library, the information provided in this tutorial and others that follow should be more than sufficient to be up and running quickly. However, if you are interested in writing your own DICOM parser from scratch, you should read this document in detail.

A Look inside the DICOM File

Every DICOM file consists of three major parts, and we will now look at the role that each part plays in the overall scheme of things.

The first part, the file header, consists of a 128-byte file preamble followed by a 4-byte prefix. This approach is very common in many other image standards such as TIFF that you may have already seen/used. The 4-byte prefix consists of the uppercase characters 'DICM' (note, it is not “DICOM”, but “DICM”). The standard does not care how the preamble should be structured and what should be stored in it. The use of the file preamble from my understanding is to simply ensure compatibility or consistency for the processing application to deal with DICOM files just like several other existing image file formats. The standard does not care about what you store in it or how you use it. So, in theory, your application could completely skip over this data when parsing the DICOM file if you chose to.

Before we look at the next part of the DICOM file, something needs to be said about the concept of transfer syntax and its role in the DICOM standard. If you recall from my earlier tutorial, the DICOM standard enables devices to transfer information with each other even if they are running on different operating systems. Different operating systems and devices follow different formats for storing data such as byte ordering when they store binary data. Due to heavy network requirements for exchange of large imagery generated from the scanning modalities such as CT or MR, the standard also has provisions to exchange image data using compression when/if necessary. In my introductory tutorial on DICOM, we also learnt about implicit and explicitVR encoding as well. All three criteria (the type of VR encoding, the type of byte ordering and the compression utilized) must first be understood and agreed upon to ensure that the two DICOM systems exchanging information understand each other during any communication between them. The transfer syntax is a set of encoding rules that helps specify this criteria through the use of UIDs which we also learnt about in my introductory tutorial on DICOM. For example, Implicit VR Little-endian (indicated by an UID value of 1.2.840.10008.1.2), Explicit VR Little-endian (value - 1.2.840.10008.1.2.1), Explicit VR Big-endian (value - 1.2.840.10008.1.2.2) and JPEG Lossless (value - 1.2.840.10008.1.2.4.57) are some of the transfer syntaxes available for DICOM processing. I will cover these in more detail when discussing image pixel data processing in a future tutorial in my DICOM series.

Now that we understand what transfer syntax is in brief (there is more to this including presentation context which I will cover in a later tutorial), let us look at the next part of the file namely the file meta-information header. This section follows immediately after the file header and consists of a data set consisting of a sequence of tagged information (called “Dicom Elements”) which specifies details such as the transfer syntax (explained above) as well as other information regarding the device or implementation that created this file and for whom this information was created for (the receiving application). See Page 32 of this document for detailed information.

Following this section is the third and last part of the DICOM file and is the data object. This part of the DICOM file is also specified in the form of a data set consisting of a series of tags which may in turn be nested and carry additional child tags themselves. These tags help carry information about the SOP instance (see my introductory tutorial for what SOP means) such as the study, the series, the patient that it belongs to as well as other details regarding the image such as image pixel data, scan position data, etc. The study, series and patient information is often used to index the image in most PACS systems for faster retrieval of data. My illustration below should hopefully provide a synopsis of the overall file structure also showing how individual DICOM elements (each element includes the tag and the associated information) are part of the whole structure.

Parts of a DICOM File

For example, in the first of the three DICOM elements in the data object section shown in my illustration above, ‘(0008, 0070)’ indicates a tag belonging to group number of 0008 with an attribute number of 0070, the ‘LO’ indicates the data type or the Value Representation (VR) as DICOM calls it (LO refers to the Long String data type), ‘PHILIPS’ is the actual value of the tag, ‘#8’ helps specify the length of the value of 8 (please note that DICOM always encodes data using an even number of characters for text so an extra padding character is used even though the value PHILIPS is only 7 characters long - more on this later), 1 represents the value multiplicity here (some data can be repeated), and ‘Manufacturer’ is the actual tag name as specified within the DICOM dictionary. The group and attribute number, the VR, the value, the value multiplicity and the tag name combined are referred to as an DICOM Element. Since the DICOM dictionary (see page 23 and onwards) implicitly defines the VR associated with each tag, the VR is redundant and is sometimes omitted. Despite this, a common practice and recommendation is to explicitly specify the VR when serializing DICOM objects into files or when exchanging DICOM information across the network. When I discuss the transfer syntax in more detail in a later tutorial, I will cover both the implicit transfer syntax (where VR is omitted) and the explicit transfer syntax (where the VR is specified along with the tag) in more detail. Now, we are in a better position to understand what an IOD (Information Object Definition) is. Let us proceed.

What do these terms really mean in DICOM - SCU, SCP, SOP and IOD?

DICOM defines the concepts of services and data that the services use or act upon. An example of a service may be a CT Store service which is responsible for storing an image generated from a CT modality to a PACS server. There are two parts to the service, the consumer of the service also known as a Service Class User or SCU, and the provider of the service also known as a Service Class Provider or SCP. In the CT Store operation for instance, the modality that generates the image acts as a C-Store SCU and transmits the data for storage to the C-Store SCP which is played by the PACS server. In the DICOM standard, the combination of the service classes and the objects that are involved with those services are known as Service Object Pairs or SOPs. The abstract definition of an SOP is called a SOP Class, and these are defined by unique identifiers (called UIDs) which I cover soon (see this link for a list of SOPs). So, the SOP CT Image Storage which is identified by a SOP Class UID of 1.2.840.10008.5.1.4.1.1.2 helps identify that this is a CT Image Storage operation. During this operation between the machines involved, there is an exchange of commands (called DIMSEs which I cover later) as well as some data which includes the image pixel information along with other identifying information such as patient, study, series and equipment information. Together, these concrete details for that operation are known as a SOP Instance. Each of these SOP instances are also identified by an unique identifier but are generated by the application responsible for transmitting them. These identifiers are called SOP Instance UIDs. The actual data involved with this SOP is defined by an IOD (Information Object Definition) specifying what DICOM modules (modules are essentially groups of DICOM elements) need to be present for successful completion of processing.

The IOD objects are themselves broken into sub groups called Information Entities (abbreviated to IE), and the Information Entities are in return broken into small groups of Information Modules. The Information Modules comprise of a series of DICOM elements which we have already seen. DICOM defines rules on what modules are mandatory, what are conditionally present as well as what are optional. The IODs themselves are classified into Normalized IODs and Composite IODs. Normalized IODs represent single objects whereas Composite IODs represents a mixture of various entities as shown in my illustration below. This is essentially the grand structure of DICOM information model in summary in my opinion. Putting this all together you will now see that any DICOM file that we have dealt so far is really an instance of an IOD (a serialized version of information) that is also transmitted between two machines during any imaging workflow. And in the case we were looking at, the operation to help store an image generated by a CT modality onto a PACS server. There is more to DICOM IODs and encoding than what is covered here, but we will deal with those areas when discussing creating DICOM files and directories. I didn’t want to bore you to death, but this is all you need to know for now.

Parts of an IOD

Fellow Oak (fo-dicom) DICOM Toolkit - Quick Overview

For the purposes of illustrating many aspects of DICOM that I plan to cover in this tutorial series, I will be using a freely available and powerful DICOM toolkit called fo-dicom DICOM Toolkit. This is a completely stand-alone DICOM toolkit that implements functionality such as DICOM file and directory processing, viewing, DICOM networking as well as DICOM object validation. This toolkit is completely free for both commercial or non-profit use. It is well documented as well. There is also a issues pages for discussion amongst users/implementors. The list of features contained within this toolkit is quite comprehensive, and my hope is to cover several DICOM concepts through a series of articles starting this one using this toolkit to implement simple but hopefully useful code illustrations. Please note that many other DICOM toolkits exist (such as dcm4che and pixelmed) and you should be able to follow along easily as many of the DICOM-related concepts I explain in my tutorials are implemented in a similar manner more or less. Also, the use of the Fellow Oak DICOM toolkit in this tutorial series does not in anyway imply my official endorsement of it for implementing a production application. Every situation is unique, and you are in the best position to decide that for yourself. This is also not meant to be a tutorial on the Fellow Oak DICOM as my focus is simply to tie DICOM concepts to things any programmer would be able to relate to. So, if your goal is to learn how to use the Fellow Oak DICOM library, I would encourage you to visit its website itself or check out its discussion forum for details.

“Out of suffering have emerged the strongest souls; the most massive characters are seared with scars.” ~ Kahlil Gibran

Before We Get Started…

Much like my previous programming examples, I will use the most bare minimum code and approach to help illustrate the concepts that I cover in this tutorial. This means that the code I write here is best suited to simply show the concept that I am trying to explain and is not necessarily the most efficient code to deploy in real life and in your production application.

To get started, you will need to configure a few things on your machine including a .NET development environment as well as the Fellow Oak (fo-dicom) DICOM library before you can run the example if you want to try this out yourself.

  1. Download a .NET IDE or Editor such as Visual Studio IDE or Visual Studio Code (even a text editor should suffice)

  2. Download the Fellow Oak DICOM library either through NuGet Package Manager or download the source code directly from here

  3. You can also find the source code used in this tutorial on GitHub

  4. You can download additional sample DICOM images from this site or from here if you want

Listing Various Tags of a DICOM file to Console

Code illustration shows a simple way to list some basic DICOM tags that are often used. There are a number of overloads and convenience methods for extracting DICOM tags in this toolkit. Feel free to explore the source code of this toolkit for additional information.

    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Text;
    using Dicom;

    namespace MakingSenseOfDicomFile
    {
        public class Program
        {
            private static readonly string PathToDicomTestFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test Files", "0002.dcm");  
            
            public static void Main(string[] args)
            {
                try
                {
                    LogToDebugConsole($"Attempting to extract information from DICOM file:{PathToDicomTestFile}...");

                    var file = DicomFile.Open(PathToDicomTestFile,readOption:FileReadOption.ReadAll);
                    var dicomDataset = file.Dataset;
                    var studyInstanceUid = dicomDataset.GetSingleValue<string>(DicomTag.StudyInstanceUID);
                    var seriesInstanceUid = dicomDataset.GetSingleValue<string>(DicomTag.SeriesInstanceUID);
                    var sopClassUid = dicomDataset.GetSingleValue<string>(DicomTag.SOPClassUID);
                    var sopInstanceUid = dicomDataset.GetSingleValue<string>(DicomTag.SOPInstanceUID);
                    var transferSyntaxUid = file.FileMetaInfo.TransferSyntax;

                    LogToDebugConsole($" StudyInstanceUid - {studyInstanceUid}");
                    LogToDebugConsole($" SeriesInstanceUid - {seriesInstanceUid}");
                    LogToDebugConsole($" SopClassUid - {sopClassUid}");
                    LogToDebugConsole($" SopInstanceUid - {sopInstanceUid}");
                    LogToDebugConsole($" TransferSyntaxUid - {transferSyntaxUid}");

                    LogToDebugConsole($"Extract operation from DICOM file successful");
                }
                catch (Exception e)
                {
                    LogToDebugConsole($"Error occured during DICOM file dump operation -> {e.StackTrace}");
                }
            }

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

Output of running the code above is shown below:

 Attempting to extract information from DICOM file...
 StudyInstanceUid - 1.3.12.2.1107.5.4.3.123456789012345.19950922.121803.6
 SeriesInstanceUid - 1.3.12.2.1107.5.4.3.123456789012345.19950922.121803.8
 SopClassUid - 1.2.840.10008.5.1.4.1.1.12.1
 SopInstanceUid - 1.3.12.2.1107.5.4.3.321890.19960124.162922.29
 TransferSyntaxUid - JPEG Baseline (Process 1): Default Transfer Syntax for Lossy JPEG 8 Bit Image Compression
 Extract information from DICOM file successful

Listing All Tags of a DICOM file to Console

Listing the DICOM file’s entire attributes is fine, but sometimes, you simply want to selectively display only specific tags contained in the DICOM file including the group and attribute number, the value representation (VR), the value, the value length, value multiplicity and the tag name information which I described earlier in this tutorial. This process is easily achieved by iterating through the DICOM data set within the DICOM file. Code illustration of the operation is show below.

    using System;
    using System.Diagnostics;
    using System.IO;
    using Dicom;

    namespace MakingSenseOfDicomFilePart2
    {
        public class Program
        {
            private static readonly string PathToDicomTestFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test Files", "0002.dcm");

            public static void Main(string[] args)
            {
                try
                {
                    LogToDebugConsole($"Attempting to extract information from DICOM file:{PathToDicomTestFile}...");

                    var file = DicomFile.Open(PathToDicomTestFile);

                    foreach (var tag in file.Dataset)
                    {
                        LogToDebugConsole($" {tag} '{file.Dataset.GetValueOrDefault(tag.Tag,0,"")}'");
                    }

                    LogToDebugConsole($"Extract operation from DICOM file successful");
                }
                catch (Exception e)
                {
                    LogToDebugConsole($"Error occured during DICOM file dump operation -> {e.StackTrace}");
                }
            }

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

Output of running the code above is shown below:

Attempting to extract information from DICOM file...
 (0008,0008) CS Image Type --> 'DERIVED'
 (0008,0016) UI SOP Class UID --> '1.2.840.10008.5.1.4.1.1.12.1'
 (0008,0018) UI SOP Instance UID --> '1.3.12.2.1107.5.4.3.321890.19960124.162922.29'
 (0008,0020) DA Study Date --> '19941013'
 (0008,0030) TM Study Time --> '141917'
 (0008,0050) SH Accession Number --> ''
 (0008,0060) CS Modality --> 'XA'
 (0008,0070) LO Manufacturer --> ''
 (0008,0080) LO Institution Name --> ''
 (0008,0081) ST Institution Address --> ''
 (0008,0090) PN Referring Physician's Name --> ''
 (0008,1030) LO Study Description --> ''
 (0008,1050) PN Performing Physician's Name --> ''
 (0008,2110) CS Lossy Image Compression (Retired) --> '01'
 (0008,2112) SQ Source Image Sequence --> ''
 (0009,0010) LO Private Creator --> 'CARDIO-SMS 1.0'
 (0009,1002:CARDIO-SMS 1.0) OB Unknown --> '0'
 (0009,1003:CARDIO-SMS 1.0) OB Unknown --> '0'
 (0009,1005:CARDIO-SMS 1.0) OB Unknown --> '0'
 (0010,0010) PN Patient's Name --> 'Rubo DEMO'
 (0010,0020) LO Patient ID --> '556342B'
 (0010,0030) DA Patient's Birth Date --> '19951025'
 (0010,0040) CS Patient's Sex --> 'M'
 (0018,0060) DS KVP --> ''
 (0018,1063) DS Frame Time --> '33'
 (0018,1152) IS Exposure --> ''
 (0018,1155) CS Radiation Setting --> 'GR'
 (0018,1500) CS Positioner Motion --> ''
 (0018,1510) DS Positioner Primary Angle --> '-32'
 (0018,1511) DS Positioner Secondary Angle --> '2'
 (0019,0010) LO Private Creator --> 'CARDIO-D.R. 1.0'
 (0019,1030:CARDIO-D.R. 1.0) UL Maximum Image Frame Size --> '262144'
 (0020,000d) UI Study Instance UID --> '1.3.12.2.1107.5.4.3.123456789012345.19950922.121803.6'
 (0020,000e) UI Series Instance UID --> '1.3.12.2.1107.5.4.3.123456789012345.19950922.121803.8'
 (0020,0010) SH Study ID --> ''
 (0020,0011) IS Series Number --> '1'
 (0020,0013) IS Instance Number --> ''
 (0020,0020) CS Patient Orientation --> ''
 (0021,0010) LO Private Creator --> 'CARDIO-D.R. 1.0'
 (0021,1013:CARDIO-D.R. 1.0) IS Image Sequence Number --> '15'
 (0028,0002) US Samples per Pixel --> '1'
 (0028,0004) CS Photometric Interpretation --> 'MONOCHROME2'
 (0028,0008) IS Number of Frames --> '96'
 (0028,0009) AT Frame Increment Pointer --> '(0018,1063)'
 (0028,0010) US Rows --> '512'
 (0028,0011) US Columns --> '512'
 (0028,0100) US Bits Allocated --> '8'
 (0028,0101) US Bits Stored --> '8'
 (0028,0102) US High Bit --> '7'
 (0028,0103) US Pixel Representation --> '0'
 (0028,1040) CS Pixel Intensity Relationship --> 'LIN'
 (0028,1090) CS Recommended Viewing Mode --> 'NAT'
 (0028,6040) US R Wave Pointer --> '20'
 (0028,6100) SQ Mask Subtraction Sequence --> ''
 (0029,0010) LO Private Creator --> 'CARDIO-D.R. 1.0'
 (0029,1000:CARDIO-D.R. 1.0) SQ Edge Enhancement Sequence --> ''
 (5000,0005) US Curve Dimensions --> '2'
 (5000,0010) US Number of Points --> '3840'
 (5000,0020) CS Type of Data --> 'ECG'
 (5000,0030) SH Axis Units --> 'DPPS'
 (5000,0103) US Data Value Representation --> '0'
 (5000,0104) US Minimum Coordinate Value --> ''
 (5000,0105) US Maximum Coordinate Value --> ''
 (5000,0106) US Curve Range --> ''
 (5000,0110) US Curve Data Descriptor --> '0'
 (5000,0112) US Coordinate Start Value --> '0'
 (5000,0114) US Coordinate Step Value --> '40'
 (5000,3000) OW Curve Data --> '255'
 (7fe0,0010) OB Pixel Data --> ''
Extract information from DICOM file successful

That is it. This is all there is to performing some rudimentary processing of a DICOM file. I hope this introductory tutorial helped you in understanding what is contained within a DICOM file. I didn’t touch on how to extract the image pixel data that is stored inside the DICOM file that we processed. I will be covering that in a future tutorial. But in my next DICOM tutorial I will show you how to encode/create a DICOM file from scratch using an image as well as some associated information. If you have any questions or comments regarding this tutorial, please feel free to send me an email. Please note that I may not get back to you right away due to work and other commitments.

Footnote: The DICOM standard restricts the file names/identifiers contained within to 8 characters (either uppercase alphabetic characters and numbers only) to keep in conformity with legacy/historical requirements. It also states that no information must be inferred/extracted from these names. The file names usually don’t have a .dcm extension when they are stored as part of a media such as CD or DVD. I use longer names to keep these details from being a distraction right now, but I still want to mention what the standard states here so that no confusion arises as a result.