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


Introduction

This is a continuation of my previous tutorial in which we looked at what a DICOMDIR file is, and also reviewed the role these special files play in the overall media management aspects related to DICOM processing. In that article, we also explored how to read and display information contained in these special files using a .NET library called "fo-dicom .NET toolkit". In this short tutorial, we will look at how to create DICOM directories and also cover some additional theory to solidify our understanding of this area.

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. You may also want 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++.

Some DICOM Theory

If you recall my earlier tutorial on DICOM association/negotiations, I mentioned that DICOM applications take on different roles when performing any DICOM-related operations. In addition to both composite and normalized DICOM operations which deal with aspects such as sending, receiving and printing of DICOM images, there is another category of operations called "storage media operations" which deal with reading or encoding content from and to storage media. These operations at their core handle data essentially as serialized files. Unlike the commands involved during composite and normalized operations, association negotiation to negotiate transfer syntaxes, etc is not feasible, and the roles played by the various actors are therefore predefined. Just like roles such as "Service Class User (SCU)" and "Service Class Provider (SCP)" which the actors play during composite and normalized DICOM operations, applications play various roles during media storage operations as well. The primary roles involved during storage media-related operations are File Set Creator (FSC), File Set Reader (FSR) and File Set Updater (FSU). For instance, any application that reads a DICOMDIR file plays the role of a "File Set Reader". And when creating a DICOMDIR file, the application plays the role of a "File Set Creator", and when updating a DICOMDIR file plays the role of a "File Set Updater". Please also note that the DICOM standard stipulates that only modification of the DICOMDIR file is permitted. No modification is permitted to the actual contents of the files or images that are referenced by the DICOMDIR file.

It is interesting to note that the DICOMDIR file itself is a valid DICOM file (uses the "Basic Directory IOD" structure) and information is encoded into it using the "Explicit VR Little Endian Transfer Syntax (UID=1.2.840.10008.1.2.1)" as no negotiation is permitted as mentioned earlier. The Basic Directory IOD structure on which DICOMDIR file is based on is organized as essentially a hierarchy of "directory records" starting with a "root directory record" which is then linked to one or more child "directory records" and these themselves may in turn have a reference to other lower level directory records. Besides the root record which carries information about the overall file (meta-information), there are essentially four types of information each of the directory records can manage ("patient", "study", "series" and "image") although not all these directory record types need to be present. Depending on the type of directory record, relevant information is encoded into it in order to allow the processing application to retrieve it for any processing. For example, a directory record dealing with the image file may carry information such as the location of the file relative to the DICOMDIR file as well as transfer syntax of the file and its abstract syntax information such as the SOP class and instance UID, etc. There is lot more to this area than I have mentioned here but I hope this should be enough to give you an idea of what happens "under the covers" so to speak. Please refer to the DICOM Standard for more information on this area. Enough theory. Let us proceed to look at how to create DICOM directories using the fo-dicom toolkit.

“Your heart is the size of an ocean. Go find yourself in its hidden depths..” ~ Rumi

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 as well DICOM networking-related operations. This toolkit is completely free for both commercial or non-profit use. The use of this toolkit in my tutorial does not in anyway imply my official endorsement of it for implementing a production application. Every situation is unique, and only you are ultimately in the best position to decide that. This article is also not meant to be a tutorial on the Fellow Oak DICOM Toolkit, and my focus here is simply to tie DICOM theory to what a practical (although simple) implementation might look like. 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 the fo-dicom issues pages for details.

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.

  • Download a .NET IDE or Editor such as Visual Studio IDE or Visual Studio Code (even a text editor should suffice)
  • Download the Fellow Oak DICOM library either through NuGet Package Manager or download the source code directly from here
  • You can also find the source code used in this tutorial on GitHub
  • You can download more DICOM images from this site if you want as well

Creating DICOMDIR Files

Like I mentioned in my previous tutorial on reading DICOMDIR files, 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. We will once again use our familiar friend, the DicomDirectory class, to create a DICOM directory file. Here, I am essentially creating an instance of this class and adding any DICOM files that are found underneath a specified location. Since the DICOM Directory is itself a DICOM file, we can utilize the many convenience extension methods that the fo-dicom library provides on the DicomMFile class to operate on it. Some of these methods that can be very useful especially for the purposes for display of information include WriteToString, WriteToConsole, WriteToLog and WriteToXml. Here, I am showing the output from the WriteToString method to display the entire contents of the DICOMDIR file.

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

    namespace UnderstandingDicomDirectoryPart3
    {
        public class Program
        {
            private static readonly string PathToDicomImages =
                Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test DICOM Images");

            static void Main(string[] args)
            {
                LogToDebugConsole("Creating Dicom directory...");

                try
                {
                    //location where we will create the DICOMDIR file from the images
                    var pathToOutputDicomDirectoryFile = Path.Combine(PathToDicomImages, "DICOMDIR");

                    var dirInfo = new DirectoryInfo(PathToDicomImages);

                    var dicomDir = new DicomDirectory();
                    foreach (var file in dirInfo.GetFiles("*.*", SearchOption.AllDirectories))
                    {
                        var dicomFile = Dicom.DicomFile.Open(file.FullName);

                        dicomDir.AddFile(dicomFile, $@"000001\{file.Name}");
                    }

                    dicomDir.Save(pathToOutputDicomDirectoryFile);

                    LogToDebugConsole($"Dicom directory creation was successful. DICOMDIR file created at '{pathToOutputDicomDirectoryFile}'");

                    var dicomDirectory = DicomDirectory.Open(pathToOutputDicomDirectoryFile);

                    LogToDebugConsole("Output the newly created DICOM directory information to console");

                    LogToDebugConsole(dicomDirectory.WriteToString());
                }
                catch (Exception ex)
                {
                  //in real-life, do something about this exception
                    LogToDebugConsole($"Error occured during Dicom directory dump. Error:{ex.Message}");
                }
            }

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

        }
    }

“May you always have open breezy spaces in your mind..” ~ Sanober Khan

The partial output of running the program above is seen below. Hopefully, this reinforces the hierarchical organization of information which I alluded to earlier (using "directory record types" and linking of these records to each other). As you can see, our newly created DICOMDIR file has a root directory record showing high level information about the DICOMDIR file itself, and the root directory record is linked to additional information which are also encoded as directory records organized by patient, then by study, then by series and then by images. Please refer to my previous tutorial on reading DICOMDIR files if you want to extract this meta data for additional processing that may be necessary to address any requirements.


Creating Dicom directory...

Dicom directory creation was successful. DICOMDIR file created at: 'C:\..\Test DICOM Images\DICOMDIR'

Outputing the newly created DICOM directory information to console

Performing Dicom directory dump:

(0002,0000) UL 172                                              #     4, FileMetaInformationGroupLength
(0002,0001) OB 0/1                                              #     2, FileMetaInformationVersion
(0002,0002) UI [1.2.840.10008.1.3.10]                           #    20, MediaStorageSOPClassUID
(0002,0003) UI [2.25.160570305858347998041621475223602997898]   #    44, 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.1]                                 #    14, ImplementationVersionName
(0004,1130) CS (no value available)                             #     0, FileSetID
(0004,1200) UL 370                                              #     4, OffsetOfTheFirstDirectoryRecordOfTheRootDirectoryEntit
(0004,1202) UL 13748                                            #     4, OffsetOfTheLastDirectoryRecordOfTheRootDirectoryEntity
(0004,1212) US 0                                                #     2, FileSetConsistencyFlag
(0004,1220) SQ Directory Record Sequence
  Item:
    > (0004,1400) UL 1122                                       #     4, OffsetOfTheNextDirectoryRecord
    > (0004,1410) US 65535                                      #     2, RecordInUseFlag
    > (0004,1420) UL 514                                        #     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 708                                        #     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 892                                        #     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]                                        #     2, InstanceNumber
  Item:
    > (0004,1400) UL 1758                                       #     4, OffsetOfTheNextDirectoryRecord
    > (0004,1410) US 65535                                      #     2, RecordInUseFlag
    > (0004,1420) UL 1246                                       #     4, OffsetOfReferencedLowerLevelDirectoryEntity
    > (0004,1430) CS [PATIENT]                                  #     8, DirectoryRecordType
    > (0010,0010) PN [CompressedSamples^CT2]                    #    22, PatientName
    > (0010,0020) LO [2CT2]                                     #     4, PatientID
    > (0010,0030) DA (no value available)                       #     0, PatientBirthDate
    > (0010,0040) CS (no value available)                       #     0, PatientSex

Conclusion

That concludes this short tutorial on creating DICOM directory files. Hopefully, I have covered enough theory for you to dig deeper into this area yourself. Please also refer to my my previous tutorial on reading DICOMDIR files for some additional testing tools that can be very useful during any software testing efforts. 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, we will move on to another interesting area of the standard namely DICOM Communications/Networking. I will start by showing how to check for DICOM connectivity using C-Echo or “DICOM ping” as it is sometimes called. See you then.