DICOM Basics using .NET and C# - Reading DICOM Directories

This is part of my series of articles on the DICOM standard. If you are totally new to DICOM, please have a quick look at my earlier article titled “Introduction to the DICOM Standard” for a quick introduction to the standard. It might also be useful to look at my other tutorial titled “DICOM Basics using .NET and C# - Making Sense of the DICOM File” to understand the structure of a DICOM file. This tutorial also assumes that you know the basics of C# or any equivalent object-oriented language such as Java or C++.

Introduction

If you are dealing with modalities such as CT or MR, you will often deal with large numbers of images generated as a result of a scan procedure (sometimes hundreds if not thousands). The PACS server or an offline archive/media such as CD/DVD may store these image sets in a nested directory structure (by patient, by study and by series). The standard restricts the file names/identifiers contained within to 8 characters (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. This makes total sense as there is a risk of attributing an image to the wrong patient, study or series and can lead to some dire consequences for diagnosis (and is reason that DICOM objects have information contained within in the form of DICOM elements or tags). However, searching through these directories and the large number of images they contain when needing to perform any diagnostic, research or error correction procedures even with these elements/tags is not a trivial task as you have to process each file individually. This is where DICOM directory files come in.

“To remain indifferent to the challenges we face is indefensible. If the goal is noble, whether or not it is realized within our lifetime is largely irrelevant. What we must do therefore is to strive and persevere and never give up.” ~ Dalai Lama XIV

The standard mandates the use of a media directory file called the DICOMDIR file (please note that this is a file and not a directory as most people mistakenly believe) which provide a number of benefits including indexing as well as summary level information regarding the files contained within (called the File-set). A File-set is simply a collection of files that share a common naming space within which any identifiers used for these files (called File IDs) have to be unique. The DICOM directory file itself is encoded as an SOP instance and has a Media Storage SOP Instance UID in its meta information similar to any other DICOM file that we have seen so far. The file must be encoded by any application creating it according to the specifications in Part 10 of the DICOM standard. The standard stipulates that the processing application should not infer anything from the order of the files in the file-set and that there should be no semantics attached to it. With the help of the DICOMDIR file, it is then possible to browse through all images on the medium of storage you are dealing with without having to read the entire directory structure and the images. This approach speeds up search times by a huge margin especially when you are dealing with large sets of files. Please see Part 10 of the DICOM standard that deals with this aspect of image management in more detail.

In this tutorial, I will show you a couple of different ways on how to read DICOM directories when developing custom software applications. In the next tutorial in this series, I will show you to write DICOM directories as well.

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

Support for DICOM Directory Operations in Fellow Oak (fo-dicom) Toolkit

The fo-dicom library provides a number of classes to perform read and write operations on DICOM directory files. These classes can be found in the Dicom.Media namespace of the library. The main class that we will use to demonstrate the read-related operation is DicomDirectory. Other supporting classes such as DicomDirectoryRecord and DicomDataset also assist in the overall operation of traversing and extracting information encoded into the DICOMDIR file. I will show several approaches of extracting information from this special file including both programmatic access as well as using a testing utility that will come in extremely handy when dealing with this file.

Approach 1 - Use Built-In Convenience Methods

The simplest way to extract information is to use the DicomDirectory class to open the file and simply call one of the convenience methods such as WriteToString, WriteToConsole, WriteToLog or WriteToXml on it. These are really extension methods implemented on the DicomFile class since the DICOMDIR is itself a DICOM file. These extension methods mentioned above simply dump the entire information contained in the file to a specified destination all at once. Here I am showing the output from WriteToString that is directed to the console. The console output seen below the code should help reinforce the idea of the four-level hierarchy by which images are organized often on DICOM media (which is by patient, then by study, then by series and then by images or instances). The console listing shows the patient information (info is anonymized here), then the study information (including study number), then the series number (and modality) followed by the image information such as the image type and the transfer syntax used to encode the image, etc. There could be multiple studies here for each patient, with each study containing multiple series of images as well. I have included a small set of DICOM images and a DICOMDIR file as part of the Visual Studio Solution if you want to explore it yourself.

    using System;
    using System.Diagnostics;
    using System.IO;
    using Dicom.Log;
    using Dicom.Media;

    namespace UnderstandingDicomDirectory
    {
        public class Program
        {
            private static readonly string PathToDicomDirectoryFile = 
                Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test Files", "DICOMDIR");

            static void Main(string[] args)
            {
                LogToDebugConsole("Performing Dicom directory dump:");

                try
                {
                    var dicomDirectory = DicomDirectory.Open(PathToDicomDirectoryFile);

                    LogToDebugConsole(dicomDirectory.WriteToString());

                    LogToDebugConsole("Dicom directory dump operation was successful");
                }
                catch (Exception ex)
                {
                    LogToDebugConsole($"Error occured during Dicom directory dump. Error:{ex.Message}");
                }
            }

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

        }
    }

Partial output of running the code above is shown below:

Performing Dicom directory dump:

(0002,0000) UL 204                                              #     4, FileMetaInformationGroupLength
(0002,0001) OB 0/1                                              #     2, FileMetaInformationVersion
(0002,0002) UI [1.2.840.10008.1.3.10]                           #    20, MediaStorageSOPClassUID
(0002,0003) UI [1.3.6.1.4.1.30071.8.220045550516071.6121351283) #    52, MediaStorageSOPInstanceUID
(0002,0010) UI [1.2.840.10008.1.2.1]                            #    20, TransferSyntaxUID
(0002,0012) UI [1.3.6.1.4.1.30071.8]                            #    20, ImplementationClassUID
(0002,0013) SH [fo-dicom 4.0.0]                                 #    14, ImplementationVersionName
(0002,0016) AE [DESKTOP-EIQC7KP]                                #    16, SourceApplicationEntityTitle
(0004,1130) CS (no value available)                             #     0, FileSetID
(0004,1200) UL 402                                              #     4, OffsetOfTheFirstDirectoryRecordOfTheRootDirectoryEntit
(0004,1202) UL 13780                                            #     4, OffsetOfTheLastDirectoryRecordOfTheRootDirectoryEntity
(0004,1212) US 0                                                #     2, FileSetConsistencyFlag
(0004,1220) SQ Directory Record Sequence
  Item:
    > (0004,1400) UL 1154                                       #     4, OffsetOfTheNextDirectoryRecord
    > (0004,1410) US 65535                                      #     2, RecordInUseFlag
    > (0004,1420) UL 546                                        #     4, OffsetOfReferencedLowerLevelDirectoryEntity
    > (0004,1430) CS [PATIENT]                                  #     8, DirectoryRecordType
    > (0008,0005) CS [ISO_IR 100]                               #    10, SpecificCharacterSet
    > (0010,0010) PN [CompressedSamples^CT1]                    #    22, PatientName
    > (0010,0020) LO [1CT1]                                     #     4, PatientID
    > (0010,0030) DA (no value available)                       #     0, PatientBirthDate
    > (0010,0040) CS [O]                                        #     2, PatientSex
  Item:
    > (0004,1400) UL 0                                          #     4, OffsetOfTheNextDirectoryRecord
    > (0004,1410) US 65535                                      #     2, RecordInUseFlag
    > (0004,1420) UL 740                                        #     4, OffsetOfReferencedLowerLevelDirectoryEntity
    > (0004,1430) CS [STUDY]                                    #     6, DirectoryRecordType
    > (0008,0005) CS [ISO_IR 100]                               #    10, SpecificCharacterSet
    > (0008,0020) DA [20040826]                                 #     8, StudyDate
    > (0008,0030) TM [185059]                                   #     6, StudyTime
    > (0008,0050) SH (no value available)                       #     0, AccessionNumber
    > (0008,1030) LO [e+1]                                      #     4, StudyDescription
    > (0020,000d) UI [1.3.6.1.4.1.5962.1.2.1.20040826185059.54) #    42, StudyInstanceUID
    > (0020,0010) SH [1CT1]                                     #     4, StudyID
  Item:
    > (0004,1400) UL 0                                          #     4, OffsetOfTheNextDirectoryRecord
    > (0004,1410) US 65535                                      #     2, RecordInUseFlag
    > (0004,1420) UL 924                                        #     4, OffsetOfReferencedLowerLevelDirectoryEntity
    > (0004,1430) CS [SERIES]                                   #     6, DirectoryRecordType
    > (0008,0005) CS [ISO_IR 100]                               #    10, SpecificCharacterSet
    > (0008,0021) DA [19970430]                                 #     8, SeriesDate
    > (0008,0031) TM [112749]                                   #     6, SeriesTime
    > (0008,0060) CS [CT]                                       #     2, Modality
    > (0020,000e) UI [1.3.6.1.4.1.5962.1.3.1.1.20040826185059.) #    44, SeriesInstanceUID
    > (0020,0011) IS [1]                                        #     2, SeriesNumber
  Item:
    > (0004,1400) UL 0                                          #     4, OffsetOfTheNextDirectoryRecord
    > (0004,1410) US 65535                                      #     2, RecordInUseFlag
    > (0004,1420) UL 0                                          #     4, OffsetOfReferencedLowerLevelDirectoryEntity
    > (0004,1430) CS [IMAGE]                                    #     6, DirectoryRecordType
    > (0004,1500) CS [000001\CT1_UNC]                           #    14, ReferencedFileID
    > (0004,1510) UI [1.2.840.10008.5.1.4.1.1.2]                #    26, ReferencedSOPClassUIDInFile
    > (0004,1511) UI [1.3.6.1.4.1.5962.1.1.1.1.1.2004082618505) #    46, ReferencedSOPInstanceUIDInFile
    > (0004,1512) UI [1.2.840.10008.1.2.1]                      #    20, ReferencedTransferSyntaxUIDInFile
    > (0008,0005) CS [ISO_IR 100]                               #    10, SpecificCharacterSet
    > (0020,0013) IS [1] 

Approach 2 - Taking Full Control by Object Traversal

The fo-dicom library also provides us with the ability to traverse through the entire information hierarchy stored in the DICOMDIR file and extract the patient, study, series and image information as required. Traversal of the information requires special conditional logic to handle the different directory records (for patient, study, series and image) that are contained within this file. The fo-dicom toolkit provides a very convenient way for navigating the information hierarchy by providing access to the data in the form of collections of data known as "directory records" through the RootDirectoryRecordCollection and the LowerLevelDirectoryRecordCollection attributes. Use of these collections is shown in the code illustration below. You should also note below that I have employed my own small helper class called OurDicomDirectoryHelper (shown underneath the main Program class) that makes it easier to extract as well as display the various applicable DICOM data attributes at each level of the information hierarchy. Since each requirement is unique, please feel free to use or extend this approach as needed (use of the Visitor Design Pattern can also be considered here).

    using System;
    using System.Diagnostics;
    using System.IO;
    using Dicom.Media;
    using UnderstandingDicomDirectory;

    namespace UnderstandingDicomDirectoryPart2
    {
        public class Program
        {
            private static readonly string PathToDicomDirectoryFile = 
                Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test Files", "DICOMDIR");

            static void Main(string[] args)
            {
                LogToDebugConsole("Performing Dicom directory dump:");

                try
                {
                    var dicomDirectory = DicomDirectory.Open(PathToDicomDirectoryFile);

                    var dicomDirectoryHelper = new OurDicomDirectoryHelper(LogToDebugConsole);

                    dicomDirectoryHelper.ShowDicomDirectoryMetaInformation(dicomDirectory);

                    foreach (var patientRecord in dicomDirectory.RootDirectoryRecordCollection)
                    {
                        dicomDirectoryHelper.Display(patientRecord);

                        foreach (var studyRecord in patientRecord.LowerLevelDirectoryRecordCollection)
                        {
                            dicomDirectoryHelper.Display(studyRecord);

                            foreach (var seriesRecord in studyRecord.LowerLevelDirectoryRecordCollection)
                            {
                                dicomDirectoryHelper.Display(seriesRecord);

                                foreach (var imageRecord in seriesRecord.LowerLevelDirectoryRecordCollection)
                                {
                                    dicomDirectoryHelper.Display(imageRecord);
                                }
                            }
                        }
                    }

                    
                    LogToDebugConsole("Dicom directory dump operation was successful");
                }
                catch (Exception ex)
                {
                    LogToDebugConsole($"Error occured during Dicom directory dump. Error:{ex.Message}");
                }
            }

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

        }
    }

As you can see below, the small helper class that I wrote helps display the DICOM data in the DICOM directory information structure. The hierarchical information contained within any directory object is managed by a special class called DicomDirectoryRecord in the fo-dicom toolkit. These directory records are ultimately what contain the relevant information about the referenced patient, study, series or image. During traversal of the information hierarchy in the DICOM directory file, these classes help provide information about the "current level" as well as additional information to help navigate to any data located in the lower hierarchial levels as/when applicable. If you are not sure what this all means and want to explore this area in more detail, please have a look at my previous tutorial titled “DICOM Basics using .NET and C# - Making Sense of the DICOM File” for more information. Again, please feel to ignore, modify or build another way of extracting this information for your purposes. Something I also want to mention here is that DICOM directories need not contain all levels necessarily or necessarily contain image-related data.


    using System;
    using Dicom;
    using Dicom.Media;

    namespace UnderstandingDicomDirectoryPart2
    {
        public class OurDicomDirectoryHelper
        {
            private readonly Action<string> _log;

            public OurDicomDirectoryHelper(Action<string> log)
            {
                _log = log;
            }
            public void ShowDicomDirectoryMetaInformation(DicomDirectory dicomDirectory)
            {
                _log($"Dicom Directory Information:");
                var fileMetaInfo = dicomDirectory.FileMetaInfo;
                _log($"Media Storage SOP Class UID: '{fileMetaInfo.MediaStorageSOPClassUID}'");
                _log($"Media Storage SOP Instance UID: '{fileMetaInfo.MediaStorageSOPInstanceUID}'");
                _log($"Transfer Syntax: '{fileMetaInfo.TransferSyntax}'");
                _log($"Implementation Class UID: '{fileMetaInfo.ImplementationClassUID}'");
                _log($"Implementation Version Name: '{fileMetaInfo.ImplementationVersionName}'");
                _log($"Source Application Entity Title: '{fileMetaInfo.SourceApplicationEntityTitle}'");
            }

            public void Display(DicomDirectoryRecord record)
            {
                switch (record.DirectoryRecordType)
                {
                    case "PATIENT":
                        ShowPatientLevelInfo(record);
                        break;
                    case "STUDY":
                        ShowStudyLevelInfo(record);
                        break;
                    case "SERIES":
                        ShowSeriesLevelInfo(record);
                        break;
                    case "IMAGE":
                        ShowImageLevelInfo(record);
                        break;
                    default:
                        _log($"Unknown directory record type: {record.DirectoryRecordType}. " +
                                $"Please check your inputs");
                        break;

                };
            }

            private void ShowImageLevelInfo(DicomDataset dataset)
            {
                _log("\t\t\tImage Level Information:");
                var values = dataset.GetValues<string>(DicomTag.ReferencedFileID);
                var referencedFileId = string.Join(@"\", values);
                _log($"\t\t\t-> Referenced File ID '{referencedFileId}'");
                //Please see https://www.dicomlibrary.com/dicom/sop/ for what these UIDs represent
                var sopClassUidInFile = dataset.GetValue<string>(DicomTag.ReferencedSOPClassUIDInFile, 0);
                _log($"\t\t\t-> Referenced SOP Class UID In File '{sopClassUidInFile}'");
                var sopInstanceUidInFile = dataset.GetValue<string>(DicomTag.ReferencedSOPInstanceUIDInFile, 0);
                _log($"\t\t\t-> Referenced SOP Instance UID In File '{sopInstanceUidInFile}'");
                var transferSyntaxUidInFile = dataset.GetValue<string>(DicomTag.ReferencedTransferSyntaxUIDInFile, 0);
                _log($"\t\t\t-> Referenced Transfer Syntax UID In File '{transferSyntaxUidInFile}'");
            }

            private void ShowSeriesLevelInfo(DicomDataset dataset)
            {
                _log("\t\tSeries Level Information:");
                var seriesInstanceUid = dataset.GetSingleValue<string>(DicomTag.SeriesInstanceUID);
                _log($"\t\t-> Series Instance UID: '{seriesInstanceUid}'");
                var modality = dataset.GetSingleValue<string>(DicomTag.Modality);
                _log($"\t\t-> Series Modality: '{modality}'");
            }

            private void ShowStudyLevelInfo(DicomDataset dataset)
            {
                _log("\tStudy Level Information:");
                var studyInstanceUid = dataset.GetSingleValue<string>(DicomTag.StudyInstanceUID);
                _log($"\t-> Study Instance UID: '{studyInstanceUid}'");
                _log($"\t-> Study ID: '{dataset.GetSingleValue<string>(DicomTag.StudyID)}'");
                _log($"\t-> Study Date: '{dataset.GetSingleValue<string>(DicomTag.StudyDate)}'");
            }

            private void ShowPatientLevelInfo(DicomDataset dataset)
            {
                _log("Patient Level Information:");
                _log($"-> Patient Name: '{dataset.GetSingleValue<string>(DicomTag.PatientName)}'");
                _log($"-> Patient ID: '{dataset.GetSingleValue<string>(DicomTag.PatientID)}'");
            }
        }
    }
        

Partial output of running the code above is shown below:


Performing Dicom directory dump:

Dicom Directory Information:
Media Storage SOP Class UID: 'Media Storage Directory Storage [1.2.840.10008.1.3.10]'
Media Storage SOP Instance UID: 'Unknown [1.3.6.1.4.1.30071.8.220045550516071.6121351283511893]'
Transfer Syntax: 'Explicit VR Little Endian'
Implementation Class UID: 'Unknown [1.3.6.1.4.1.30071.8]'
Implementation Version Name: 'fo-dicom 4.0.0'
Source Application Entity Title: 'DESKTOP-EIQC7KP'

Patient Level Information:
-> Patient Name: 'CompressedSamples^CT1'
-> Patient ID: '1CT1'
	Study Level Information:
	-> Study Instance UID: '1.3.6.1.4.1.5962.1.2.1.20040826185059.5457'
	-> Study ID: '1CT1'
	-> Study Date: '20040826'
		Series Level Information:
		-> Series Instance UID: '1.3.6.1.4.1.5962.1.3.1.1.20040826185059.5457'
		-> Series Modality: 'CT'
			Image Level Information:
			-> Referenced File ID '000001\CT1_UNC'
			-> Referenced SOP Class UID In File '1.2.840.10008.5.1.4.1.1.2'
			-> Referenced SOP Instance UID In File '1.3.6.1.4.1.5962.1.1.1.1.1.20040826185059.5457'
			-> Referenced Transfer Syntax UID In File '1.2.840.10008.1.2.1'
Patient Level Information:
-> Patient Name: 'CompressedSamples^CT2'
-> Patient ID: '2CT2'
	Study Level Information:
	-> Study Instance UID: '1.3.6.1.4.1.5962.1.2.2.20040826185059.5457'

“Anger is an acid that can do more harm to the vessel in which it is stored than to anything on which it is poured.” ~ Mark Twain

Testing Tools for DICOM Directory Troubleshooting

Yet another way of troubleshooting and verifying DICOM directory information is to use one of the many useful DICOM testing tools out there. The one that I have used in the past and have liked is DCMTK. The toolkit comes with many standalone testing utilities that help you test various aspects of DICOM through a command line interface. The dcmdump command is the one that I will use here to show the contents of the DICOM directory.

Create DICOM Directory Results

That concludes this short tutorial on reading DICOM directory files. DICOMDIR is a useful feature, and it provides information in a compact form about all the files contained on any DICOM storage media. This helps a processing application or user to select or browse through relevant images without having to parse through every single file on the media by having to query their individual attributes. This is can be a time saver especially when you are dealing with large sets of DICOM files which is often the case in a clinical setting. However, please be cautious on the use of DICOMDIR files because some files in the various folders maybe have been added after the DICOM directory file was originally created, and so, things can go out of sync easily. Lot of multimedia applications these days scan entire directories in the order of only milliseconds to determine content within them, and hence, going that route may not be a bad idea as well. I will leave that to you to decide.

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. In my next tutorial in this series, I will cover how you can create/write DICOM directories as well as review some additional theory on storage media operations, the various roles that DICOM applications can play while interacting with DICOM media and also how these DICOMDIR files are structured on the inside. See you then.