Introduction to DICOM

110
Introduction to DICOM Chapter 1: Introduction DICOM is a software integration standard that is used in Medical Imaging. All modern medical imaging systems (AKA Imaging Modalities) Equipment like X-Rays, Ultrasounds, CT (Computed Tomography), and MRI (Magnetic Resonance Imaging) support DICOM and use it extensively. In this tutorial I present a high level review of DICOM. We will look at DICOM from the user point of view trying to avoid the fine details when possible. Readers familiar with the DICOM standard and its technical vocabulary will surely recognize these terms though I will try to avoid them when there exists a common replacement. The reason for this is because the DICOM standard’s vocabulary is very different from the equivalent terms used in everyday’s computing and I try here to explain DICOM to people with common background in modern software and computing but none or very little background in Medical Imaging and Healthcare IT. The core of DICOM is a file format and a networking protocol. DICOM File Format – All Medical Images are saved in DICOM format. Medical Imaging Equipment creates DICOM files. Doctors use DICOM Viewers , computer software applications that can display DICOM images, to diagnose the findings in the images. DICOM files contain more than just images. Every DICOM file holds patient information (name, ID, sex and birth date), important acquisition data (e.g., type of equipment used and its settings), and context of the imaging study that is used to link the image to the medical treatment it was part of. DICOM Network Protocol – All medical imaging applications that are connected to the hospital network use the DICOM protocol to exchange information, mainly DICOM images but also patient and procedure information. The DICOM network protocol is used to search for imaging studies in the archive and restore imaging studies to the workstation in order to display it. There are also more advanced network commands that are used to control and follow the treatment, schedule procedures, report statuses and share the workload between doctors and imaging devices. Just like every web browser can display JPEG pictures stored on far away servers, medical systems that use DICOM can send and receive DICOM images and search for them in other medical systems. DICOM is first of all an Interface Definition. It’s success relies on the ability to integrate medical systems manufactured by many different vendors. The reality today in medical imaging is that when installing new 1

Transcript of Introduction to DICOM

Page 1: Introduction to DICOM

Introduction to DICOM

Chapter 1: Introduction

DICOM is a software integration standard that is used in Medical Imaging. All modern medical imaging systems (AKA Imaging Modalities) Equipment like X-Rays, Ultrasounds, CT (Computed Tomography), and MRI (Magnetic Resonance Imaging) support DICOM and use it extensively.

In this tutorial I present a high level review of DICOM. We will look at DICOM from the user point of view trying to avoid the fine details when possible.Readers familiar with the DICOM standard and its technical vocabulary will surely recognize these terms though I will try to avoid them when there exists a common replacement. The reason for this is because the DICOM standard’s vocabulary is very different from the equivalent terms used in everyday’s computing and I try here to explain DICOM to people with common background in modern software and computing but none or very little background in Medical Imaging and Healthcare IT.

The core of DICOM is a file format and a networking protocol.

DICOM File Format – All Medical Images are saved in DICOM format. Medical Imaging Equipment creates DICOM files. Doctors use DICOM Viewers, computer software applications that can display DICOM images, to diagnose the findings in the images. DICOM files contain more than just images. Every DICOM file holds patient information (name, ID, sex and birth date), important acquisition data (e.g., type of equipment used and its settings), and context of the imaging study that is used to link the image to the medical treatment it was part of.

DICOM Network Protocol – All medical imaging applications that are connected to the hospital network use the DICOM protocol to exchange information, mainly DICOM images but also patient and procedure information. The DICOM network protocol is used to search for imaging studies in the archive and restore imaging studies to the workstation in order to display it. There are also more advanced network commands that are used to control and follow the treatment, schedule procedures, report statuses and share the workload between doctors and imaging devices.

Just like every web browser can display JPEG pictures stored on far away servers, medical systems that use DICOM can send and receive DICOM images and search for them in other medical systems.

DICOM is first of all an Interface Definition. It’s success relies on the ability to integrate medical systems manufactured by many different vendors.

The reality today in medical imaging is that when installing new imaging equipment in the hospital and plugging it into the network, it can immediately query the medical imaging archive (PACS), retrieve images that were created by other systems and display them. Additionally, if the new system produces images, they can be reviewed on other vendor’s systems that are already members of the network. All this is done without any changes or modifications to any of the involved system software.

Some of you would rightfully say that this is exactly what you would expect from any new laptop or printer you bring home. However, for the medical community, this was almost impossible before DICOM. Integrating medical equipment of different vendors used to be a big issue. Even today with all the advancement that IT made, very large budgets are spent over interfaces and integration in every large project, not only in medicine.

The ability of modern imaging equipment to seamlessly collaborate and integrate together in a multi-vendor environment is the most notable achievement of DICOM that led to a great advancement in medical imaging.

1

Page 2: Introduction to DICOM

Chpater 2 - Why is it this way in DICOM?Many times when I explain features and aspects of DICOM I get questions like, “Why do you need DICOM if you have JPEG and XML?”; or, ”why is DICOM so complicated?”. Many variants of such questions continually come up over and over again. These types of questions can be very broad or very specific and relate to all kind of choices that the people who write the standard make and the options that they take.

Here’s an example. In DICOM there’s a command called C-FIND that is used to make queries to another system, to search for images and other entities. You would naturally think of SQL, but in DICOM there’s a different way of doing it.

Let’s continue with the C-FIND example. The DICOM C-FIND command has three variants, each with its own data model. These variants are ‘Patient Root’, ‘Study Root’ and ‘Patient Study Only Root’. I’m always asked why do we need three variants and I really can’t give a simple answer. Luckily, the third model, ‘Patient Study Only Root’ is now obsolete so we are left with only two models.

There are many more examples like the C-FIND Query Model where DICOM just have too many options and redundancies. But as the standard became more and more common and dominant, developers just took care of all the options and degenerate the redundancies. Many PACS implementation simply treat all three models using the same code. The answer that I have come to use for that kind of questions is:"That’s the way it is in DICOM and let’s just get on with it"

Chapter 3 – DICOM Elements

Let’s start with a useful example. Suppose you are a dermatologist and that you use your Smartphone digital camera to record and track patients’ skin condition. You practice a simple procedure that is basically this:

1. Take a photo

2. Send it to yourself by email

3. Open the email on your laptop and save the picture in a folder having the patient name.

As programmers, we don’t have to talk much about the flows of this practice but for a small, one doctor clinic, this might just work.

In this lesson, we’ll take the JPEG image and DICOMIZE it. When we DICOMIZE an Image we wrap the image in a DICOM envelope and add important data that is required by the DICOM standard in order to enable all DICOM enabled applications to read and display the image correctly. It’s true that non DICOM application can display the JPEG image just as it is now without DICOMIZING but that’s another story.

We will use RZDCX DICOM Toolkit to convert the image to a DICOM object. Then we’ll ‘dump’ the content of the DICOM object into a text file in order to see how DICOM objects are structured.

To convert to DICOM using RZDCX we’ll use the following three lines of C# code:

private void Convert() {

DCXOBJ o = new DCXOBJ();

2

Page 3: Introduction to DICOM

string jpegImage = "my_image.jpg";

o.SetJpegFrames(jpegImage);

}

Now let’s see what we have in the file after that by adding a small dump to text file so our function now looks like this:

private void Convert() {

DCXOBJ o = new DCXOBJ();

string jpegImage = "my_image.jpg";

o.SetJpegFrames(jpegImage);

o.Dump("my_image.txt");

}

If we now look at the content of the file my_image.txt we'll see this:

# Dicom-Data-Set# Used TransferSyntax: UnknownTransferSyntax(0008,0016) UI =SecondaryCaptureImageStorage # 26, 1 SOPClassUID(0028,0002) US 3 # 2, 1 SamplesPerPixel(0028,0004) CS [YBR_FULL_422] # 12, 1 PhotometricInterpretation(0028,0006) US 0 # 2, 1 PlanarConfiguration(0028,0010) US 96 # 2, 1 Rows(0028,0011) US 372 # 2, 1 Columns(0028,0100) US 8 # 2, 1 BitsAllocated(0028,0101) US 8 # 2, 1 BitsStored(0028,0102) US 7 # 2, 1 HighBit(0028,0103) US 0 # 2, 1 PixelRepresentation(0028,2110) CS [01] # 2, 1 LossyImageCompression(0028,2114) CS [ISO_10918_1] # 12, 1 LossyImageCompressionMethod(7fe0,0010) OB (PixelSequence #=2) # u/l, 1 PixelData (fffe,e000) pi (no value available) # 0, 1 Item (fffe,e000) pi ff\d8\ff\db\00\43\00\08\06\06\07\06\05\08\07\07\07\09\09\08\0a\0c... # 9624, 1 Item(fffe,e0dd) na (SequenceDelimitationItem) # 0, 0 SequenceDelimitationItemLet’s go over this dump and see what’s in it. The lines starting with a hash (#) are comments and we’ll ignore them.

3

Page 4: Introduction to DICOM

DICOM ElementsA DICOM object is comprised of DICOM elements or DICOM attributes. Every line in the above dump represents one element.Every DICOM Element has a Tag, a Data Type called VR (acronym for Value Representation), Length and Value. In the dump above the lines starts with the tag number (gggg,eeee), then the VR Code, then the value (strings are printed in square brackets) and then a hash sign (#) followed by the element value length, a comma, then Value multiplicity (which we'll talk about later) and the tag name. The tag name and multiplicity are add by the dump method. The way DICOM encodes elements is shown in this drawing taken from the DICOM standard, chapter 5.

Illustration of DICOM element encoding in a DICOM data stream Image takes from the DICOM standard, Chapter 5.

4

Page 5: Introduction to DICOM

TagsEvery DICOM element has a Tag that uniquely defines the element and its properties, much like a bar code defines a product in the supermarket. The DICOM tag is comprised of two short numbers called Group and Element. DICOM Tags that are related to one another sometimes have the same group. In our example you can see many elements from group 0028. This is the Image group. These are attributes of the image and are used to describe the image properties. For example (0028,0010) is Rows element and it is the height of the image. (0028,0011) is the Columns and it is the width of the image in pixels. There are more elements of the image group in our example and we’ll describe them in detail when we talk about interpreting and displaying DICOM Images.

Value RepresentationThe VR is represented as two character code. The VR defines the data type of the element. In our example you can see UI for Unique identifier, US for Unsigned Short, CS for Coded String and OB for Other Byte i.e. a byte stream.Because every element has a Tag, the tag implicitly defines the VR. For example the Rows element (Tag 0028,0010) is always US (Unsigned Short). This is why the VR is usually redundant and can be omitted. However, the common practice and IHE recommendation is to explicitly state the VR when serializing DICOM objects into files or into network buffers. We’ll talk more about that when we discuss Implicit and Explicit Transfer Syntax.

Value LengthBecause DICOM is a binary protocol (in contrast to textual protocols such as html and xml) elements have length. DICOM elements length are always even. Even if the element’s value is a single character string like Patient Sex (0010,0040) that is either ‘M’ for Male or ‘F’ for Female or ‘O’ for Other, the element length should be 2 and the value will be padded by a space (ASCII 0x20). String types (like CS and UI) are padded by space and binary types like US are padded by null 0x0.

Summary

DICOM Elements are the building blocks of the DICOM standards. They are used in DICOM files and in network communication.

Every element has a unique Tag that specifies what’s in the element and its data type. DICOM Elements are typed. The DICOM data types are called VR. Every Element has even length, even if it's value length is odd. Strings are padded with a space

and binary data with a null.

Adding Elements to an ObjectWe conclude this example with a short code snippet showing how to add elements to a DICOM object using RZDCX.RZDCX takes care for you about the details so you simply create a new element, initialize it with the Tag and set the value. Here's the code:

DCXOBJ o = new DCXOBJ(); DCXELM e = new DCXELM();

// Manufecturer

e.Init((int)DICOM_TAGS_ENUM.Manufacturer);

e.Value = "RZ - Software Services";

5

Page 6: Introduction to DICOM

o.insertElement(e);

In the next chapter we'll add more data elements to the DICOM object we've created to make it a valid DICOM object and then save it to a file and we'll discuss the differences between DICOM objects and DICOM files.

Chapter 4 – DICOM ObjectsIn chapter 3 we’ve learned about DICOM elements. Every element is one piece of typed data with a pre defined, well specified meaning. There are thousands of DICOM elements (See chapter 6 of the standard) from the very basic attributes of patient name and birth date to the most esoteric uses of 3D surface vortices. In this chapter we’re going to collect elements into image object that is called Secondary Capture Image.

The guys at DICOM did a lot of very good work and created well defined classes for a very detailed Data Model. This is why I always advise to dig in the DICOM standard before designing your imaging device software because there’s a very good chance that the DICOM technical committees already did the work for you and you can save a lot of expansive design time this way.

In a way DICOM objects definitions are similar to object oriented programming. I prefer though the analog to interfaces specifications. The motivation to adhere to a standard is to enable interoperability. By detailing information object definitions (IOD’s) DICOM enables us to exchange virtual objects between applications without knowing in advance anything about the application we are going to interface with.

In this chapter I'm going to complete chapter’s 3 examples by adding elements to the object until it’s a valid Secondary Capture Image according to the DICOM standard. Secondary Capture Image is the simplest DICOM image object. Secondary Captures is not related to any specific device. It has the very basic set of elements that a DICOM application needs in order to display and archive a DICOM image properly.

The DICOM Data Model

The specification of DICOM objects are documented in chapter 3 of the DICOM standard that defines the DICOM data model. In its most simplified form the DICOM Data Model looks like this.

A simplified view of the DICOM Data Model

The data model defines Information Entities (IE’s); Patient, Study, Series and Image. There are more IE’s like Visit, Equipment, Clinical Trial, Procedure and many others and they are all defined in chapter 3 which is the longest chapter of the standard. The DICOM Data Model that is made of IE's is normalized.

6

Page 7: Introduction to DICOM

It is a perfect relational database definition. The classes of the DICOM Objects however are composites made of modules from different entities. The integration is achieved by applications that exchange composite objects between one another. Each application is responsible for it's own internal normalized database that is private to itself and should not interest any other application and is out of the standard's scope. The way you build your DICOM application internals is completely your business. The only thing that matters is your interfaces. Your application should talk proper DICOM. By the way, the DICOM network protocol that we’ll get to in later chapters also makes a distinction between Normalized and Composite operations and there’s N protocol and C protocol with different commands for each one.

The classes of the DICOM static data model are called SOP Classes and are defined by IOD’s – Information Object Definition. IOD’s are specified in Appendix A of chapter 3 of the standard. An IOD is a collection of Modules and a Module is a collection of elements from one information entity that together represent something. The modules are also defined in chapter 3 of the DICOM standard in appendix C. Two object oriented concepts, composition and reuse, that are used by DICOM is the Modules that are parts shared between different IOD’s.

All DICOM Objects must include the SOP Common Module and modules from the four main IE’s: Patient, Study, Series and Image (Image and Instance are the same in DICOM. Once there were only images but then objects that are not images has been defined and the name thus changed from Image to Instance in order to represent an instance of a SOP class). All DICOM Images, that is DICOM Instances that Are Images, must include the Image Module. Because Every DICOM Object must be part of a Series, all DICOM Objects must include the General Series Module and because all series must be part of a Study, every DICOM Object must include the General Study Module and because every study is made on some patient, all DICOM objects must have a Patient Module. You probably wonder what SOP means? That's an acronym for "Service Object Pair" and please take my word for it, that for now this is all we need to say about that. Maybe when we talk about DICOM Services or understanding DICOM Conformance Statements I'll try to explain where this name comes from, but I'm not sure that it makes much difference. In a word, SOP is a pair of a DICOM Sevice and and DICOM Object like Secondary Capture Object and Storage Service.

Secondary Capture Image IOD

With that understanding at hand, let’s look at the SC Image IOD Modules from section A.8.1.3 of the standard. It looks like this:

7

Page 8: Introduction to DICOM

IE Module Reference UsagePatient Patient C.7.1.1 M

Clinical Trial Subject

C.7.1.3 U

Study General Study C.7.2.1 MPatient Study C.7.2.2 UClinical Trial Study

C.7.2.3 U

Series General Series C.7.3.1 MClinical Trial Series

C.7.3.2 U

Equipment General Equipment

C.7.5.1 U

SC Equipment C.8.6.1 MImage General Image C.7.6.1 M

Image Pixel C.7.6.3 MDevice C.7.6.12 USpecimen C.7.6.22 USC Image C.8.6.2 MOverlay Plane C.9.2 UModality LUT C.11.1 UVOI LUT C.11.2 USOP Common C.12.1 M

Taking out all the lines marked with a U that mark these modules as User optional and leaving only the M lines that stand for Mandatory modules and we are left with eight modules that one of them is actually empty as you will soon find out.

IE Module Reference UsagePatient Patient C.7.1.1 MStudy General Study C.7.2.1 MSeries General Series C.7.3.1 MEquipment SC Equipment C.8.6.1 MImage General Image C.7.6.1 M

Image Pixel C.7.6.3 MSC Image C.8.6.2 MSOP Common C.12.1 M

Note also that in this table there are only two modules that are specific to SC and all the other modules are general and common modules that are shared by many IOD’s. It's very common in DICOM that the mandatory elements are very few and there are a lot of optional elements. This is not unique to the DICOM standard and is common to many standards and sometime leads to uncertainties and ambiguities or gaps as we sometime call them. These gaps led IHE to publish technical frameworks that further specify the use of the standard and narrow the options. The IHE initiative is a great success and participating in IHE connect-a-thon is an outstanding opportunity to test systems in a very realistic integration environment.

During the design of your application, when you need to add some data to an object and don’t find a proper place for it, remember these optional modules that we’ve omitted here and look for a place to put your data in them before defining private elements, modules and objects. DICOM let you define new elements that are called private elements and we’ll look at that later on but as already said more than once, there’s a very good chance that these guys have already did the work for you and defined a solution to your problem and went through the process of validating it and it is probably documented very well in the standard. After all, the DICOM standard is more than 3,000 pages long.

8

Page 9: Introduction to DICOM

Element Types

To finish our digging in the DICOM standard, we now need to replace every line in the modules table with the module's definition from appendix C of chapter 3 of the DICOM standard. If you look at the standard you will see that every module is rather large and includes many elements but luckily, like the optional modules, many of the elements are optional too. In the modules tables elements are marked with a ‘Type’ column that can be 1 for Mandatory with actual value (not zero length), 2 for Mandatory that can be null (zero length) or 3 for optional. 1 and 2 can also have a ‘C’ for conditional so 1C is mandatory if some condition, that is detailed in the module table, is met and the same for 2C.

So we are now going to copy the modules but only the 1 and 2 elements dumping all the 3’s and also 1C and 2C’s that their condition is obviously not met for our example. Remember that we are talking interoperability so striving to the simplest object that as many systems as possible can understand is our goal.

Next to every element I’m going to add the C# code to add it into the object and at the end we’re going to have all the code at hand.

Unique Identifiers (UID’s)

One last thing, before we dive into the code, I’d like to say a word about Unique Identifiers. DICOM makes extensive use of Unique Identifiers. Almost every entity in the DICOM Data Model has a unique identifier. In DICOM every SOP Class have its UID. All pre-define UID’s including the SOP Class UID’s are documented in chapter 6 of the DICOM standard. A DICOM Object is an Instance of such class and is called SOP Instance and it also has a UID called SOP Instance UID. DICOM defines a mechanism in order to make sure UID’s are globally Unique. Every DICOM application should acquire a ‘root’ UID that is used as a prefix to the UID’s it creates. Every entity in the DICOM Data Model also has a UID with the exception of the patient. Patients are identified using the combination of their name and ID. Studies, Series, all have UID’s. DICOM Archives (PACS) should use the UID’s to index their databases so when other applications make searches (Queries) they can refer to objects using the UID’s and the archive can respond to the searches quickly.

As I said earlier, every DICOM Image Object has patient, study, series, and image modules. In our example we’ll generate new UID’s for Series Instance UID and SOP Instance UID (that is the Image UID). In "proper" DICOM integration the Study Instance UID is provided by the department IT system (RIS/PACS) through a DICOM service called Modality Worklist but devices can default to creating the Study Instance UID if it’s not provided from an external system. The Series Instance UID and SOP Instance UID are always generated by the Imaging device. The definition of a DICOM series is a set of DICOM Instances that were generated together by the same equipment at the same operation. You can read the exact definition in section A.1.2.3 of chapter 3 of the standard.

9

Page 10: Introduction to DICOM

Creating a Secondary Capture DICOM Image

// create the objectDCXOBJ o = new DCXOBJ();

Table C.7-1PATIENT MODULE ATTRIBUTESAttribute Name

Tag Type

Attribute Description

// Create elementDCXELM e = new DCXELM();

Patient's Name

(0010,0010)

2 Patient's full name.

// Patient name // But we set it to "DOE^JOHN"e.Init((int)DICOM_TAGS_ENUM.PatientsName);o.insertElement(e);

Patient ID

(0010,0020)

2 Primary hospital identification number or code for the patient.

// Patient ID - type 2 can be emptye.Init((int)DICOM_TAGS_ENUM.patientID);o.insertElement(e);

Patient's Birth Date

(0010,0030)

2 Birth date of the patient.

e.Init((int)DICOM_TAGS_ENUM.PatientBirthDate);o.insertElement(e);

Patient's Sex

(0010,0040)

2 Sex of the named patient. Enumerated Values: M = male F = female O = other

e.Init((int)DICOM_TAGS_ENUM.PatientsSex);o.insertElement(e);

Table C.7-3GENERAL STUDY MODULE ATTRIBUTESAttribute Name

Tag Type

Attribute Description

Study Instance UID

(0020,000D)

1 Unique identifier for the Study.

// Let's assume we got a Study Instance// UID from the department IT system// and that it's 1.2.3.4.5.6.7e.Init((int)DICOM_TAGS_ENUM.studyInstanceUID);e.Value = "1.2.3.4.5.6.7";o.insertElement(e);

Study Date

(0008,0020)

2 Date the Study started.

// Let's say the study was created now e.Init((int)DICOM_TAGS_ENUM.StudyDate);e.Value = DateTime.Now;o.insertElement(e);

10

Page 11: Introduction to DICOM

Study Time

(0008,0030)

2 Time the Study started.

e.Init((int)DICOM_TAGS_ENUM.StudyTime);e.Value = DateTime.Now;o.insertElement(e);

Referring Physician's Name

(0008,0090)

2 Name of the patient's referring physician

e.Init((int)DICOM_TAGS_ENUM.ReferringPhysicianName);o.insertElement(e);

Study ID (0020,0010)

2 User or equipment generated Study identifier.

e.Init((int)DICOM_TAGS_ENUM.StudyID);o.insertElement(e);

Accession Number

(0008,0050)

2 A RIS generated number that identifies the order for the Study.

e.Init((int)DICOM_TAGS_ENUM.AccessionNumber);o.insertElement(e);

Table C.7-5a GENERAL SERIES MODULE ATTRIBUTESAttribute Name

Tag Type

Attribute Description

Modality (0008,0060)

1 Type of equipment that originally acquired the data used to create the images in this Series. See C.7.3.1.1.1 for Defined Terms.

// Modality is type 1 in // Generalseries module but// it is also type 3 in SC // Equipment module that// overtakes in this case so we // can leave it out of the object

Series Instance UID

(0020,000E)

1 Unique identifier of the Series.

// Let's generate a // Series Instance UIDDCXUID uid = new DCXUID();e.Init((int)DICOM_TAGS_ENUM.seriesInstanceUID);e.Value = uid.CreateUID(UID_TYPE.UID_TYPE_SERIES);o.insertElement(e);

Series Number

(0020,0011)

2 A number that identifies this Series.

e.Init((int)DICOM_TAGS_ENUM.SeriesNumber);o.insertElement(e);

Laterality (0020,0060)

2C

Laterality of (paired) body part examined. Required if the

e.Init((int)DICOM_TAGS_ENUM.Laterality);o.insertElement(e);

11

Page 12: Introduction to DICOM

body part examined is a paired structure and Image Laterality (0020,0062) or Frame Laterality (0020,9072) are not sent. Enumerated Values: R = right L = leftNote: Some IODs support Image Laterality (0020,0062) at the Image level or Frame Laterality(0020,9072) at the Frame level in the Frame Anatomy functional group macro, which can provide a more comprehensive mechanism for specifying the laterality of the body part(s) being examined.

12

Page 13: Introduction to DICOM

Table C.8-24SC EQUIPMENT MODULE ATTRIBUTESAttribute Name

Tag Type

Attribute Description

Conversion Type

(0008,0064)

1 Describes the kind of image conversion. Defined Terms : DV = Digitized Video DI = Digital Interface DF = Digitized Film WSD = Workstation SD = Scanned Document SI = Scanned Image DRW = Drawing SYN = Synthetic Image

e.Init((int)DICOM_TAGS_ENUM.ConversionType);e.Value = "DRW"; o.insertElement(e);

Modality (0008,0060)

3 Source equipment for the image. This type definition shall override the definition in the General Series Module.See C.7.3.1.1.1 for Defined Terms.

// See comment above about Modality

Note that here I left in a type 3 element named Modality. Read the description and see why. What it says here is that a SC image doesn’t have to have a Modality tag.Table C.7-9GENERAL IMAGE MODULE ATTRIBUTESAttribute Name

Tag Type

Attribute Description

Instance Number

(0020,0013)

2 A number that identifies this image.Note: This Attribute was

e.Init((int)DICOM_TAGS_ENUM.InstanceNumber);o.insertElement(e);

13

Page 14: Introduction to DICOM

named Image Number in earlier versions of this Standard.

Patient Orientation

(0020,0020)

2C

Patient direction of the rows and columns of the image. Required if image does not require Image Orientation (Patient) (0020,0037) and Image Position (Patient) (0020,0032). May be present otherwise. See C.7.6.1.1.1 for further explanation. Note: IOD’s may have attributes other than Patient Orientation, Image Orientation, or Image Position (Patient) to describe orientation in which case this attribute will be zero length.

// Let's assume the condition// for all the 2C's bellow// is not met

Content Date

(0008,0023)

2C

The date the image pixel data creation started. Required if image is part of a series in which the images are temporally related.Note: This Attribute was formerly known as

14

Page 15: Introduction to DICOM

Image Date.Content Time

(0008,0033)

2C

The time the image pixel data creation started. Required if image is part of a series in which the images are temporally related.

Table C.7-11bIMAGE PIXEL MACRO ATTRIBUTES

// The Image Pixel module is// completely set by RZDCX when // we insert the bitmap or jpeg// See chapter 3 of this tutorial// We'll discuss this module later ono.SetBMPFrames("my_image.bmp");

Attribute Name

Tag Type

Attribute Description

Samples per Pixel

(0028,0002)

1 Number of samples (planes) in this image. See C.7.6.3.1.1 for further explanation.

Photometric Interpretation

(0028,0004)

1 Specifies the intended interpretation of the pixel data. See C.7.6.3.1.2 for further explanation.

Rows (0028,0010)

1 Number of rows in the image.

Columns (0028,0011)

1 Number of columns in the image

Bits Allocated

(0028,0100)

1 Number of bits allocated for each pixel sample. Each sample shall have the same number of bits allocated. See PS 3.5 for further explanation.

Bits Stored

(0028,0101)

1 Number of bits stored for

15

Page 16: Introduction to DICOM

each pixel sample. Each sample shall have the same number of bits stored. See PS 3.5 for further explanation.

High Bit (0028,0102)

1 Most significant bit for pixel sample data. Each sample shall have the same high bit. See PS 3.5 for further explanation.

Pixel Representation

(0028,0103)

1 Data representation of the pixel samples. Each sample shall have the same pixel representation. Enumerated Values: 0000H = unsigned integer. 0001H = 2's complement

Pixel Data

(7FE0,0010)

1C

A data stream of the pixel samples that comprise the Image. See C.7.6.3.1.4 for further explanation.Required if Pixel Data Provider URL (0028,7FE0) is not present.

Planar Configuration

(0028,0006)

1C

Indicates whether the pixel data are sent color-by-plane or color-by-pixel. Required if Samples per Pixel (0028,0002)

16

Page 17: Introduction to DICOM

has a value greater than 1. See C.7.6.3.1.3 for further explanation.

Note: in the Image Pixel Module I left out all the 1C elements related to Palette because we’re going to create here a RGB image.Table C.8-25SC IMAGE MODULE ATTRIBUTESNote: all the elements in this module are type 3.Table C.12-1SOP COMMON MODULE ATTRIBUTESAttribute Name

Tag Type

Attribute Description

SOP Class UID

(0008,0016)

1 Uniquely identifies the SOP Class. See C.12.1.1.1 for further explanation. See also PS 3.4.

// The SOP Class UID of// SC Image is e.Init((int)DICOM_TAGS_ENUM.sopClassUid);e.Value = "1.2.840.10008.5.1.4.1.1.7";o.insertElement(e);

SOP Instance UID

(0008,0018)

1 Uniquely identifies the SOP Instance. See C.12.1.1.1 for further explanation. See also PS 3.4.

// We've instanciated a DCXUID// above. Let's use it again// to create a SOP Instance UIDe.Init((int)DICOM_TAGS_ENUM.sopInstanceUID);e.Value = uid.CreateUID(UID_TYPE.UID_TYPE_INSTANCE);o.insertElement(e);

Specific Character Set

(0008,0005)

1C

Character Set that expands or replaces the Basic Graphic Set.Required if an expanded or replacement character set is used.See C.12.1.1.2 for Defined Terms.

// Let's default to latin 1// and leave it out

Now it’s time to add a dump command at the end of this code and see what we’ve got. Here’s the complete function:

public DCXOBJ CreateSCImage()

17

Page 18: Introduction to DICOM

{ // create the object DCXOBJ o = new DCXOBJ(); // Create element DCXELM e = new DCXELM(); // Patient name // But we set it to "DOE^JOHN" e.Init((int)DICOM_TAGS_ENUM.PatientsName); o.insertElement(e); // Patient ID - type 2 can be empty e.Init((int)DICOM_TAGS_ENUM.patientID); o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.PatientBirthDate); o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.PatientsSex); o.insertElement(e);

// Let's assume we got a Study Instance // UID from the department IT system // and that it's 1.2.3.4.5.6.7 e.Init((int)DICOM_TAGS_ENUM.studyInstanceUID); e.Value = "1.2.3.4.5.6.7"; o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.PatientsSex); o.insertElement(e);

// Let's say the study was created now e.Init((int)DICOM_TAGS_ENUM.StudyDate); e.Value = DateTime.Now; o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.StudyTime); e.Value = DateTime.Now; o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.ReferringPhysicianName); o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.StudyID); o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.AccessionNumber); o.insertElement(e);

// Modality is type 1 in // Generalseries module but // it is also type 3 in SC // Equipment module that // overtakes in this case so we // can leave it out of the object

// Let's generate a // Series Instance UID DCXUID uid = new DCXUID();

18

Page 19: Introduction to DICOM

e.Init((int)DICOM_TAGS_ENUM.seriesInstanceUID); e.Value = uid.CreateUID(UID_TYPE.UID_TYPE_SERIES); o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.SeriesNumber); o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.Laterality); o.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.ConversionType); e.Value = "DRW"; o.insertElement(e);

// See comment above about Modality

e.Init((int)DICOM_TAGS_ENUM.InstanceNumber); o.insertElement(e);

// Let's assume the condition // for all the 2C's bellow // is not met

// The Image Pixel module is // completely set when // we insert set the bitmap or jpeg // See chapter 3 of this tutorial. // We'll discuss this module later on o.SetBMPFrames("my_image.bmp");

// The SOP Class UID of // SC Image is e.Init((int)DICOM_TAGS_ENUM.sopClassUid); e.Value = "1.2.840.10008.5.1.4.1.1.7"; o.insertElement(e);

// We've instanciated a DCXUID // above. Let's use it again // to create a SOP Instance UID e.Init((int)DICOM_TAGS_ENUM.sopInstanceUID); e.Value = uid.CreateUID(UID_TYPE.UID_TYPE_INSTANCE); o.insertElement(e);

// Let's default to latin 1 // and leave the character set out

// Let's dump the object to text file o.Dump("my_image.txt");

// And don't forget to save it too o.saveFile("my_image.dcm");

return o; }

19

Page 20: Introduction to DICOM

And here’s the dump:

# Dicom-Data-Set# Used TransferSyntax: UnknownTransferSyntax(0008,0016) UI =SecondaryCaptureImageStorage # 26, 1 SOPClassUID(0008,0018) UI [2.16.124.113543.6021.1.3.3727584845.2784.1322776593.2] # 54, 1 SOPInstanceUID(0008,0020) DA [20111201] # 8, 1 StudyDate(0008,0030) TM [235633.000] # 10, 1 StudyTime(0008,0050) SH (no value available) # 0, 0 AccessionNumber(0008,0064) CS [DRW] # 4, 1 ConversionType(0008,0090) PN (no value available) # 0, 0 ReferringPhysicianName(0010,0010) PN (no value available) # 0, 0 PatientName(0010,0020) LO (no value available) # 0, 0 PatientID(0010,0030) DA (no value available) # 0, 0 PatientBirthDate(0010,0040) CS (no value available) # 0, 0 PatientSex(0020,000d) UI [1.2.3.4.5.6.7] # 14, 1 StudyInstanceUID(0020,000e) UI [2.16.124.113543.6021.1.2.3727584845.2784.1322776593.1] # 54, 1 SeriesInstanceUID(0020,0010) SH (no value available) # 0, 0 StudyID(0020,0011) IS (no value available) # 0, 0 SeriesNumber(0020,0013) IS (no value available) # 0, 0 InstanceNumber(0020,0060) CS (no value available) # 0, 0 Laterality(0028,0002) US 3 # 2, 1 SamplesPerPixel(0028,0004) CS [RGB] # 4, 1 PhotometricInterpretation(0028,0006) US 0 # 2, 1 PlanarConfiguration(0028,0010) US 50 # 2, 1 Rows(0028,0011) US 50 # 2, 1 Columns(0028,0100) US 8 # 2, 1 BitsAllocated(0028,0101) US 8 # 2, 1 BitsStored(0028,0102) US 7 # 2, 1 HighBit(0028,0103) US 0 # 2, 1 PixelRepresentation(7fe0,0010) OB ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff... # 7500, 1 PixelData

Couple of notes about this dump (highlighted).

First you can see the UID of this instance. Whenever you run this code again, a new UID will be generated. The same goes for Series Instance UID.

See the (7fe0,0010) element? This is the pixel data. The pixels from the bitmap image we’ve converted. It is 7500 bytes long because the image I converted is 50x50 and there's 3 bytes per pixel (Red, Green and Blue).

Look at Study Date (0008,0020) and Study Time (0008,0030). The DA (Date) VR (Value Representation) is defined as formatted date string YYYYMMDD and the TM (Time) VR is a time formatted string with format HHMMSS.TTT. You can see that I ran this example four minutes before midnight on December 1st 2011.

Here’s a short quiz:

Look at the length Photometric Interpretation (0028,0004). The value is “RGB” but the value length is 4 bytes. Why?

This was quite long and complicated chapter covring the DICOM Data Model, Information Entities, Modules, Information Objects Definitions (IOD's), SOP Classes and SOP Instances, Unqiue Identifiers and element types. Most important, we've walked through the process of reading chapter 3 of the standard and translating it to a software that creates the DICOM Object according to the standard specifications. I hope it wasn't too complicated. As always, comments and questions are most welcome.

20

Page 21: Introduction to DICOM

Chapter 5 – Solving a DICOM Communication Problem

Today we are going to diagnose a communication problem between two DICOM applications and hopefully find the reason for the problem and solve it. I know, we didn’t even start talking about the DICOM network protocol, but hay, we’re not going to read all this 3,000 pages standard together before getting our hands dirty, right?In this post we'll discuss:

1. Application Entities (AE’s) – the nodes in the DICOM network and their name – AE Title2. Association – a network peer-to-peer session between two DICOM applications3. Association Negotiation – The first part of the association in which the two AE’s agree on what

can and can’t be done during the Association4. The Verification Service using the C-ECHO command – a DICOM Service Class that is used to

verify a connection, sort of application level ‘ping’.5. The Storage Service using the C-STORE command – a DICOM Service that allows one AE to send

a DICOM object to another AE

The C in C-ECHO and C-STORE commands stands for Composite. If you remember, in chapter 4 when discussing the DICOM Data Model, we said that DICOM applications exchange composite objects (the DICOM images that we already know) that are composites of modules from different IE's where IE's are the information entities of the Normalized DICOM data model.

Here's the story:Complaint 20123

Burt Simpson from Springfield Memorial Hospital reports that he can’t send the screen capture to the PACS. He kept clicking the green “Send” button but he always gets the same error: “Operation Failed!”. The log file Burt copied from the system is attached.

You may ask yourself, what’s the point in analyzing a log of an application that we are never going to use? Well, the truth is that all DICOM logs look alike. Actually, most DICOM applications are quite similar because DICOM software implementations have common ancient ancestors. If it’s a C library it may be the DICOM test node, CTN. If it’s Java than it might be dcm4che. Even if it's PHP or other newer languages, the libraries were transcribed and ported from the old C implementations so all DICOM logs are similar.

The log file in this case, named DICOM-20111207-093017.log, is 250MB long and when you double click it notepad hangs for couple of minutes before crashing. When you open the log using EXCEL you see the same pattern repeating 100 times, one time for every click Burt made, and after isolating one repetition you see this relatively short pattern with exactly four log entries that we’re going to analyze together.

2011-12-1022:22:25.906000 1508 INFO Association Request Parameteres:Our Implementation Class UID: 2.16.124.113543.6021.2Our Implementation Version Name: RZDCX_2_0_1_8Their Implementation Class UID: Their Implementation Version Name: Application Context Name: 1.2.840.10008.3.1.1.1Calling Application Name: RZDCXCalled Application Name: PACSResponding Application Name: resp AP TitleOur Max PDU Receive Size: 32768Their Max PDU Receive Size: 0Presentation Contexts: Context ID: 1 (Proposed) Abstract Syntax: =VerificationSOPClass Proposed SCP/SCU Role: Default

21

Page 22: Introduction to DICOM

Accepted SCP/SCU Role: Default Proposed Transfer Syntax(es): =LittleEndianExplicit =BigEndianExplicit =LittleEndianImplicit Context ID: 3 (Proposed) Abstract Syntax: =SecondaryCaptureImageStorage Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Proposed Transfer Syntax(es): =LittleEndianExplicitRequested Extended Negotiation: noneAccepted Extended Negotiation: none

2011-12-1022:22:26.062000 1508 INFO Association Request Result: NormalAssociation Response Parameteres:Our Implementation Class UID: 2.16.124.113543.6021.2Our Implementation Version Name: RZDCX_2_0_1_8Their Implementation Class UID: 1.2.826.0.1.3680043.2.60.0.1Their Implementation Version Name: softlink_jdt103Application Context Name: 1.2.840.10008.3.1.1.1Calling Application Name: RZDCXCalled Application Name: PACSResponding Application Name: PACSOur Max PDU Receive Size: 32768Their Max PDU Receive Size: 32768Presentation Contexts: Context ID: 1 (Accepted) Abstract Syntax: =VerificationSOPClass Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Accepted Transfer Syntax: =LittleEndianImplicit Context ID: 3 (Abstract Syntax Not Supported) Abstract Syntax: =SecondaryCaptureImageStorage Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: DefaultRequested Extended Negotiation: noneAccepted Extended Negotiation: none

2011-12-1022:22:31.234000 1508 INFO Can't store object because SOP Class was not negotiated or not accepted by peer. SOP Class UID: 1.2.840.10008.5.1.4.1.1.7, SOP Instance UID: 2.16.124.113543.6021.1.3.3727584845.5056.1323548540.2

2011-12-1022:22:31.234000 1508 ERROR In DCXREQ, Code: 520, Text: DIMSE No valid Presentation Context IDStopped Logging.

The problem is clearly stated in the third log entry and marked as error in the fourth entry. It says that the peer application, the one we want to send our image to refuse to store this type of object. Some toolkits do provide additional helpful information. However, we could have guessed that this will be the problem already in the second entry of the log in the association request response where the presentation context for secondary captured was marked as not supported by the called AE.

The log above is from the following short C# function that I’ve written for this post:

public void SendSCImage(DCXOBJ o) {

22

Page 23: Introduction to DICOM

DCXAPP app = new DCXAPP(); app.LogLevel = LOG_LEVEL.LOG_LEVEL_INFO; app.StartLogging("DICOM.log"); try { DCXREQ req = new DCXREQ(); req.SendObject("RZDCX", "PACS", "localhost", 6104, o); } catch (Exception e) { MessageBox.Show(e.Message); }

app.StopLogging(); }

Together with the function CreateSCImage that we’ve written together in chapter 4 we have this little program that creates a Secondary Image in memory and then attempts sending it:

DCXOBJ o = CreateSCImage(); SendSCImage(o);

Before analyzing the log, let’s go over the code of SendSCImage and make sure that we understand it.The first three lines creates a DCXAPP class, sets the log level to one less than the highest level (which is ‘Debug’). The DCXAPP class is used to control RZDCX’s global settings. Once it goes out of scope, the settings remains.Then we have the try-catch block that is very straight forward. We create a DCXREQ class and use it to send the object we’ve created using the SendObject method. DCXREQ is a DICOM requester – a DICOM application that initiates DICOM network with another application and sends DICOM commands. SendObject takes five (5) parameters and encapsulates the whole world of DICOM networking in it. All this log was generated by this single method because it does all the work of DICOM networking for you and that’s exactly what’s unique in RZDCX, that you don’t have to deal with all these details. Still, it’s good to know what’s inside so when things gets messy you’ll have a clue about what might have gone wrong.Like all the other networking methods of DCXREQ, the first four (4) parameters of SendObject are used to establish the DICOM network connection with the remote DICOM application.The first parameter is our Application Entity Title. In the DICOM network every node is an Application Entity (AE) and the node name is AE Title. You might ask why do we need an AE title if we have a server name or IP address and the answer is that an AE Title is sort of alias for the combination of IP address and port number. We can run many DICOM applications on a single server. I can run two instances of my PACS on the same computer, one listening on port 104 which is the standard TCP/IP port reserved for DICOM communication and another one listening on port 1104. Each application can be completely independent of the other. I can run as many DICOM applications as I like all having the same IP address. BTW, DICOM is almost always used in LAN environment and I strongly discourage anyone from using DICOM in WAN environment though I know some people do this but it’s really not a good idea. DICOM protocol is internal, private, in your local network, preferably in its own dedicated subnet.AE Titles are case sENsItIVE, 16 characters max.The second parameter is the AE title of the application that we would like to connect to. We sometime call it the target application or called AE Title or responding AE or simply the peer.The third parameter is the server name (or IP address) of the server that the called AE runs on.The fourth parameter is the port number the called AE listens on.That concludes the parameters that are common to all DCXREQ network methods. With these parameters we can start an ‘Association’ with the called AE.The new term that’s interesting here is Association. What’s that? That’s like a network session. It’s a frame that the conversation with the called AE is going to take place in.

23

Page 24: Introduction to DICOM

We can divide the DICOM network communication into two parts. The first part is setting up the Association and the second part is exchanging DICOM commands.99% of the difficulties in DICOM networking are related the first part – the association negotiation. Even if this stage passed and we start exchanging commands, the chances are that problems are because of faults in the first part.

The fifth parameter is the object we would like to send.SendObject does the following:

1. Start a TCP/IP connection2. Negotiates the association parameters to agree what can be done during the association3. Send the DICOM object4. Close the association5. Close the TCP/IP connection

Let’s go back to the log and have a look at the first part of the log now. Here it is:2011-12-1022:22:25.906000 1508 INFO Association Request Parameteres:Our Implementation Class UID: 2.16.124.113543.6021.2Our Implementation Version Name: RZDCX_2_0_1_8Their Implementation Class UID: Their Implementation Version Name: Application Context Name: 1.2.840.10008.3.1.1.1Calling Application Name: RZDCXCalled Application Name: PACSResponding Application Name: resp AP TitleOur Max PDU Receive Size: 32768Their Max PDU Receive Size: 0

This part of the log is a textual dump of the first information that was sent to the called AE and is called Association Request. It’s a collection of parameters that describe our application, its capabilities and its intentions in this session.Every log entry in RZDCX log starts with a timestamp, a thread ID (1508 in this case) and the log level of the entry (INFO in this case). In the complete log above I’ve highlighted the timestamps at the beginning of every log entry.The first element of in the association request identifies our DICOM implementation.

Our Implementation Class UID: 2.16.124.113543.6021.2Our Implementation Version Name: RZDCX_2_0_1_8

In this case it’s the RZDCX UID and version number. It’s always interesting because DICOM toolkits and systems have their own little glitches so if you know some system has a problem you already identified and you see that you are dealing with the same implementation, you know how to deal with it. It’s also important when communicating with the other application vendor to report the application version.In the request dump we see only our implementation info but further down the log in the response dump we will see the identification of the called AE.Then we have the application context name. This is a UID that is reserved for DICOM. It’s always the same.

Application Context Name: 1.2.840.10008.3.1.1.1

Next we have the AE titles: the calling AE title and the called AE title.

Calling Application Name: RZDCXCalled Application Name: PACS

24

Page 25: Introduction to DICOM

Note that this is just the request so it’s the values we passed to SendObject. In the response we will have also what they sent us back. Usually the application that respond to the association request should check that the called AE is matching to its own AE and that the calling AE is something that is found in its configuration file or database. If it doesn’t match, than the called AE can reject the association.Then we have the Max PDU Size. PDU is an application level ‘packet’ that says how big is the buffer we are willing to consume for each request.

Our Max PDU Receive Size: 32768

In this case we propose no more than 32K. One known problem is that some applications send an association request so big that the called AE can’t consume. We’ll see in a minute why they do that and how to avoid it.The next chunk of the log is still part of the first entry in the log. The association request includes a list of DICOM services. The items in this list are called presentation contexts:

Presentation Contexts: Context ID: 1 (Proposed) Abstract Syntax: =VerificationSOPClass Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Proposed Transfer Syntax(es): =LittleEndianExplicit =BigEndianExplicit =LittleEndianImplicit Context ID: 3 (Proposed) Abstract Syntax: =SecondaryCaptureImageStorage Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Proposed Transfer Syntax(es): =LittleEndianExplicitRequested Extended Negotiation: noneAccepted Extended Negotiation: none

We’ve sent a list with two items. Each item is a presentation context and identifies a DICOM Service that we wish to use during this association. The presentation contexts are oddly numbered. The first is 1, the second is 3 and a third would have been 5. Why? I don’t know. That’s the way it is. As I said, they are oddly numbered.So the first service we’ve asked for is Verification. It is performed using the DICOM command C-ECHO. In the log we see this:

Abstract Syntax: =VerificationSOPClass

Every service has a UID. In the log file UID’s that are known are replaced by their name. The verification service is a sort of high level ping. It’s a DICOM command called C-ECHO that when sent the peer should respond with a success status. Note that we have not sent a C-ECHO command yet. We just asked the called AE in our association request to use it in the second part. We also didn’t say we will send a C-ECHO. A DICOM application that listens on a port and waits for incoming connections must always implement the verification service. Our little application is not listening on any port yet. At this stage, we only play the client role here and connect to another application. As a client, It’s always a good habit to ask for the verification service. If we don’t ask for it and the application we connect to does not support any of the other services that we ask for than it will hang up on us. By adding the verification to our request we force the server to say yes on at least one thing we ask for.

The second service we’ve asked for is Secondary Capture Image Storage:

25

Page 26: Introduction to DICOM

Abstract Syntax: =SecondaryCaptureImageStorage

If you remember when we talked about SOP Class UID in chapter 4, I said that SOP is a pair of a service and an object definition. So here we have this combination. We are asking the peer application to store an object that we are going to send and we tell it that the object is going to be a Secondary Capture Image. If we also had another object type, for example a CT Image, than we would have had to ask for a third presentation context for it. The called AE can allow or disallow each one of the services. So it’s possible to create an application that accepts specific types of objects. For example, if we are writing a 3D reconstruction workstation for CT scans we can accept only CT images and thus force the sending application to send us only that type of objects. However, this is not such a good idea because applications tend to send complete studies and there may be in a study images of different classes, for example there may be one series with a CT scan and another one with a report and another one with radiation dose report and if we limit our workstation to accept only CT images than the application that were implemented to send complete studies will keep reporting failures because they can’t send the other objects even though the CT images that we needed has arrived. A better design would be to allow all object types and ignore the ones we don’t need.

This mechanism of negotiating every type of object led some vendors to the very bad habit of simply requesting all the possible objects they know. This can lead to a 50K long association request and if the called AE implementation can read only 32K long requests it can easily crash on the simplest buffer overflow bug. Additionally, sending a 50K association request every time you just want to check a connection by using a C-ECHO command is pure waste of time.

RZDCX’s SendObject negotiates only the required SOP Class UID’s. The DCXREQ Send method sends a set of DICOM files. First it goes over all the files, create a list of all their SOP Classes and then negotiates this list with the called AE.

This concludes our association request. We identified ourselves and stated what we are calling for. Now let’s see what the called AE is going to say. After the association request is sent, the called AE reads the request and sends back an association response. It is almost identical to the request. The called AE simply fills in the form we sent. The second entry in this log is a dump of this response.

2011-12-1022:22:26.062000 1508 INFO Association Request Result: NormalAssociation Response Parameteres:Our Implementation Class UID: 2.16.124.113543.6021.2Our Implementation Version Name: RZDCX_2_0_1_8Their Implementation Class UID: 1.2.826.0.1.3680043.2.60.0.1Their Implementation Version Name: softlink_jdt103Application Context Name: 1.2.840.10008.3.1.1.1Calling Application Name: RZDCXCalled Application Name: PACSResponding Application Name: PACSOur Max PDU Receive Size: 32768Their Max PDU Receive Size: 32768Presentation Contexts: Context ID: 1 (Accepted) Abstract Syntax: =VerificationSOPClass Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Accepted Transfer Syntax: =LittleEndianImplicit Context ID: 3 (Abstract Syntax Not Supported) Abstract Syntax: =SecondaryCaptureImageStorage Proposed SCP/SCU Role: Default

26

Page 27: Introduction to DICOM

Accepted SCP/SCU Role: DefaultRequested Extended Negotiation: noneAccepted Extended Negotiation: none

From the timestamp you can see that it came back just 1 tenth of a second after the request was sent and that the response status is Normal (I highlighted the parameters that before were empty or has changed). You can also see that now their implementation identification is filled in with the value softlink_jdt103 which identifies a very handy Java utility package from Tiani. Their AE title is indeed “PACS” and they have accepted our association request. There is couple of cases here. One case is that they simply don’t answer. In this case our request will time out without getting any response. Another case is what we have here that is the association request was accepted and we are now connected to the called AE. The third case is that the called AE decides it doesn’t want to talk to us and sends an Association Reject response. For example if its AE title is not “PACS” so it would probably say “Wrong called AE title”. The reason for rejection is encoded in the response status and sometimes has an additional textual explanation.We also got the list of services back. The verification was accepted but the Secondary Capture Storage was not. This means that if we like we can send a C-ECHO command but we can’t send our Secondary Capture Image using a C-STORE. Because this was what we wanted to do in this association you see the next two log entries:

2011-12-1022:22:31.234000 1508 INFO Can't store object because SOP Class was not negotiated or not accepted by peer. SOP Class UID: 1.2.840.10008.5.1.4.1.1.7, SOP Instance UID: 2.16.124.113543.6021.1.3.3727584845.5056.1323548540.2

2011-12-1022:22:31.234000 1508 ERROR In DCXREQ, Code: 520, Text: DIMSE No valid Presentation Context IDStopped Logging.

It means that we can’t store the object because the peer doesn’t support this service. Yippy! We actually could figure out what’s wrong ha?! Now Burt can go to the PACS admin and ask him why his PACS can’t store Secondary Captures and the PACS admin is probably going to ask Burt to which server he tried connecting and to what port and then say that port 6104 is the Worklist Manager that serve Modality Worklist and Performed Procedure Step requests (which are DICOM services we’ll learn about later on) and that if we want to send something to the PACS we should try connecting to port 104. Case solved.OK, let’s run this again and this time connect to port 104. It’s a good idea to have the AE title, IP address and port number of the called AE configurable in our application so we don’t have to compile every time. Most DICOM applications have such configuration. Usually it’s a table with at least the columns: AE Title, host and port and maybe an id and a comment. Here’s the log of a successful sending, this time the log level was set to Debug.

2011-12-1512:22:51.000000 4296 INFO Association Request Parameteres:Our Implementation Class UID: 2.16.124.113543.6021.2Our Implementation Version Name: RZDCX_2_0_1_8Their Implementation Class UID: Their Implementation Version Name: Application Context Name: 1.2.840.10008.3.1.1.1Calling Application Name: RZDCXCalled Application Name: PACSResponding Application Name: resp AP TitleOur Max PDU Receive Size: 32768Their Max PDU Receive Size: 0Presentation Contexts: Context ID: 1 (Proposed) Abstract Syntax: =VerificationSOPClass Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Proposed Transfer Syntax(es):

27

Page 28: Introduction to DICOM

=LittleEndianExplicit =BigEndianExplicit =LittleEndianImplicit Context ID: 3 (Proposed) Abstract Syntax: =SecondaryCaptureImageStorage Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Proposed Transfer Syntax(es): =LittleEndianExplicitRequested Extended Negotiation: noneAccepted Extended Negotiation: none

2011-12-1512:22:51.000000 4296 DEBUG Constructing Associate RQ PDU2011-12-1512:22:51.000000 4296 DEBUG WriteToConnection, length: 310, bytes written: 310, loop no: 12011-12-1512:22:51.015000 4296 DEBUG PDU Type: Associate Accept, PDU Length: 216 + 6 bytes PDU header 02 00 00 00 00 d8 00 01 00 00 50 41 43 53 20 20 20 20 20 20 20 20 20 20 20 20 52 5a 44 43 58 20 20 20 20 20 20 20 20 20 20 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 10 00 00 15 31 2e 32 2e 38 34 30 2e 31 30 30 30 38 2e 33 2e 31 2e 31 2e 31 21 00 00 19 01 00 00 00 40 00 00 11 31 2e 32 2e 38 34 30 2e 31 30 30 30 38 2e 31 2e 32 21 00 00 1b 03 00 00 00 40 00 00 13 31 2e 32 2e 38 34 30 2e 31 30 30 30 38 2e 31 2e 32 2e 31 50 00 00 3b 51 00 00 04 00 00 80 00 52 00 00 1c 31 2e 32 2e 38 32 36 2e 30 2e 31 2e 33 36 38 30 30 34 33 2e 32 2e 36 30 2e 30 2e 31 55 00 00 0f 73 6f 66 74 6c 69 6e 6b 5f 6a 64 74 31 30 332011-12-1512:22:51.015000 4296 INFO Association Request Result: NormalAssociation Response Parameteres:Our Implementation Class UID: 2.16.124.113543.6021.2Our Implementation Version Name: RZDCX_2_0_1_8Their Implementation Class UID: 1.2.826.0.1.3680043.2.60.0.1Their Implementation Version Name: softlink_jdt103Application Context Name: 1.2.840.10008.3.1.1.1Calling Application Name: RZDCXCalled Application Name: PACSResponding Application Name: PACSOur Max PDU Receive Size: 32768Their Max PDU Receive Size: 32768Presentation Contexts: Context ID: 1 (Accepted) Abstract Syntax: =VerificationSOPClass Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Accepted Transfer Syntax: =LittleEndianImplicit Context ID: 3 (Accepted) Abstract Syntax: =SecondaryCaptureImageStorage Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Accepted Transfer Syntax: =LittleEndianExplicitRequested Extended Negotiation: noneAccepted Extended Negotiation: none

28

Page 29: Introduction to DICOM

2011-12-1512:22:51.031000 4296 DEBUG DIMSE Command To Send:

# Dicom-Data-Set# Used TransferSyntax: UnknownTransferSyntax(0000,0000) UL 0 # 4, 1 CommandGroupLength(0000,0002) UI =SecondaryCaptureImageStorage # 26, 1 AffectedSOPClassUID(0000,0100) US 1 # 2, 1 CommandField(0000,0110) US 1 # 2, 1 MessageID(0000,0700) US 0 # 2, 1 Priority(0000,0800) US 1 # 2, 1 DataSetType(0000,1000) UI [2.16.124.113543.6021.1.3.3727584845.720.1323944568.6] # 52, 1 AffectedSOPInstanceUID

2011-12-1512:22:51.031000 4296 DEBUG DIMSE sendDcmDataset: sending 146 bytes2011-12-1512:22:51.031000 4296 DEBUG WriteToConnection, length: 12, bytes written: 12, loop no: 12011-12-1512:22:51.031000 4296 DEBUG WriteToConnection, length: 146, bytes written: 146, loop no: 12011-12-1512:22:51.031000 4296 DEBUG DIMSE sendDcmDataset: sending 7894 bytes2011-12-1512:22:51.031000 4296 DEBUG WriteToConnection, length: 12, bytes written: 12, loop no: 12011-12-1512:22:51.031000 4296 DEBUG WriteToConnection, length: 7894, bytes written: 7894, loop no: 12011-12-1512:22:51.046000 4296 INFO DIMSE receiveCommand2011-12-1512:22:51.062000 4296 INFO DIMSE receiveCommand: 1 pdv's (178 bytes), presID=32011-12-1512:22:51.062000 4296 DEBUG DIMSE Command Received:

# Dicom-Data-Set# Used TransferSyntax: LittleEndianImplicit(0000,0002) UI =SecondaryCaptureImageStorage # 26, 1 AffectedSOPClassUID(0000,0100) US 32769 # 2, 1 CommandField(0000,0120) US 1 # 2, 1 MessageIDBeingRespondedTo(0000,0800) US 257 # 2, 1 DataSetType(0000,0900) US 45056 # 2, 1 Status(0000,0902) LO [set InstanceNumber to 0] # 24, 1 ErrorComment(0000,1000) UI [2.16.124.113543.6021.1.3.3727584845.720.1323944568.6] # 52, 1 AffectedSOPInstanceUID

2011-12-1512:22:51.062000 4296 DEBUG WriteToConnection, length: 10, bytes written: 10, loop no: 1Stopped Logging.

The storage command did pass but we got back a warning status (45056 = 0xB000) instead of success (0x0000). We also got a warning comment that the called AE changed Instance Number element from null to 0, maybe in order to index it properly in its database.

We should have talked about transfer syntaxes but this is already a long post so I’ll leave transfer syntaxes for another time.

Let’s summarize what we’ve covered in this post.

1. The nodes in the DICOM network are called Application Entities (AE) and are identified using a case sensitive name called AE Title.

2. DICOM communication is always between two AE’s i.e. it is peer-to-peer.3. The DICOM ‘session’ is called Association4. The association is divided into two stages. The first stage is called Association Negotiation. In

the second stage the two AE’s exchange DICOM commands.5. In the Association Negotiation, the requesting AE sends a list of presentation contexts that

identify DICOM services it wishes to use and the responding AE sends back the same list

29

Page 30: Introduction to DICOM

marked with which services it accepted and can be used and which it declined and can’t be used in this association.

6. The verification service is an application level service used to verify communication between two AE's

7. The storage service is used to transfer DICOM objects between AE's. The storage service is negotiated separately for every SOP Class. For example an application can allow storage of CT image and forbid storage of MR images. This is a not a good design though.

That’s it. I hope you still believe me that DICOM is Easy. As always, comments are most welcome.

Chapter 6 - Transfer Syntax

Transfer syntax defines how DICOM objects are serialized. When holding an object in memory, the only thing that matter is that your application can use it. The internal representation of the object is your own business. However, when sharing objects with other applications, everyone should be able to use the same object. The common solution for such problems is serialization.

Serialization is the process of writing a data structure or object state to wire i.e in a format that can be stored in a file or memory buffer, or transmitted across a network so it can be red on the other side of the wire or later by the same or by another process.

There's no shared memory in DICOM but it can be easily made using the same mechanism that is utilized for networking and files alike i.e. serializing the object into memory according to the rules dictated by the standard i.e. using transfer syntax.

In this post I'll cover the following issues:

Present the term Transfer Syntax, Why Transfer Syntax is required What is Transfer Syntax used for How Transfer Syntax is set when using

o DICOM files o DICOM network

So, as I said, the serialization in DICOM is governed by a term called Transfer Syntax.

Transfer Syntax is defined at the object level and is the syntax for serializing a DICOM object. We have seen transfer syntaxes already in chapter 5 when dealing with association negotiation but did not discuss them. In order for an application to read a DICOM object from a network wire, it has to know the rules that were used to write the object into the wire. In the association request the calling AE sends a list of abstract syntaxes with SOP Class UID's. For every SOP Class, the calling AE sends a list of transfer syntax UID's. In the association response the called AE selects one of the transfer syntax UID's for every SOP class it accepts.

Here's a short snippet from the last post on DICOM networking:

Presentation Contexts: Context ID: 1 (Proposed) Abstract Syntax: =VerificationSOPClass Proposed SCP/SCU Role: Default Accepted SCP/SCU Role: Default Proposed Transfer Syntax(es): =LittleEndianExplicit =BigEndianExplicit

30

Page 31: Introduction to DICOM

=LittleEndianImplicit

This is part of the association request and in red you see the three trasnfer syntaxes that the calling application is suggesting for the first presentation context. It suggests the three basic transfer syntaxes:

Little Endian Explicit which is defined be the UID: 1.2.840.10008.1.2.1, Big Endian Explicit which is defined be the UID: 1.2.840.10008.1.2.2 and Little Endian Implicit which is defined by the UID: 1.2.840.10008.1.2

The Transfer Syntax UID is a UID that identify the transfer syntax (that's lame, ha?). Like all the other UID's it can be found in chapter 6 of the standard.

Transfer syntax sets exactly three things that are required in order to parse the serialized DICOM object:

1. If VR's are explicit, i.e. if the data type code of every element should be serialized or it will be implicitly deduced from the element tag (see the post on DICOM Elements)

2. The order that bytes of multi-byte data types are serialized. For example, if we have an element with unsigned short data type (the Value Representation, VR, is US) than which byte of the two is the first byte written to the buffer and which is the second.

3. If pixel data is compressed and what compression algorithm is used. Compressed pixel data transfer syntax are always explicit VR little Endian (so you can call JPEG baseline 1.2.840.10008.1.2.4.50 for example "explicit little endian jpeg baseline") .

Most DICOM toolkits, and RZDCX is not different, handle the first two items on this list for you and automatically change the byte order and inserts or removed the VR codes for you. But there are cases when this multitude of choises (and don't ask why do we need three serialization syntaxes, instead read the second post in this series) causes problems.

I'm going to leave compression for a later chapter but in short, DICOM defines many compressed transfer syntaxes, that are simply the compressed image stream encapsulated into the pixel data element of the DICOM object so one can actually open a DICOM file with a binary editor, locate the pixel data element (7FE0,0010), cut out the value, save it as a jpeg file, double click it and see it. Maybe we'll do it together when talking about compression.

Let's now do an example that shows some issues that you may be confusing. Let's say we have two images, both are CT but one we have compressed with the jpeg lossless compression and we would like to send it to an archive.

This negotiation is rather strange because one can for example negotiate two abstract syntaxes (1 and 3, remember?) in the following way:

1) CT Image storage, explicit little endian3) CT Image storage, jpeg losslessIn this example the calling application requests to send a CT image and a compressed CT image.The request could have been composed this way as well:1) CT Image storage, (explicit little endian, jpeg lossless)But this is different because the called AE will select one of the suggested transfer syntaxes and the calling AE will have to send all CT images according to the selected transfer syntax either encoding them all before sending or decoding them all, depending on what transfer syntax the called AE have selected. If your application can't do this compression on the fly, you may get calls from the field. Using the first negotiation however, the called AE will most likely accept both 1 and 3 and we can send the uncompressed images using context id 1 and the jpeg compressed DICOM images using context id 3. You don't mind that I say that RZDCX takes care of all this for you.

Most issues with transfer syntax are related to applications that don't support transfer syntaxes that

31

Page 32: Introduction to DICOM

others require. If you have one application that can read only big endian and another that is limited to little endian, they will never talk to one another. That's radical but there are many applications that don't support any compressed images or can only store them but not display them and if your application generates only jpeg's so you better rethink the design.Transfer syntax issues sometimes cause images to look bad. I've seen applications that change the big-little endian (i.e. the byte order) of the pixel data without changing the transfer syntax properly causing the images to be unreadable. Such images usually feature jagged edges and bad contrast. I've also seen a very popular CD burner that if interfaced with implicit syntax causes many elements to become of Unknown VR even if these are well defined elements.

Now let's move to DICOM files. Just like in DICOM networking, DICOM files must be red by all applications so thats a serialization too. Here are the rules for DICOM files:

When writing a DICOM object to file, the application that creates the file writes it in the following way:

1. The first 128 bytes are null (0x00) 2. Bytes 128 - 131 (zero based) are 'DICM' which is the DICOM magic number 3. Add to the object a file meta header - a group of elements of group 0002 that are the first

elements in the object. 4. Group 0002 is written in Little Endian Explicit 5. Element (0002,0010) is the Transfer Syntax UID that is used for all the elements other than

group 2.

So, when reading a DICOM file, a DICOM library should do this:

1. Read 132 bytes (these 132 bytes are called the preamble) and see that 128-131 equal "DICM" 2. Start parsing using Little Endian Explicit all the group 0002 elements 3. Check the value of element (0002,0010) and use this transfer syntax for the rest of the file.

Important: always remove group 0002 before sending objects over the network. Group 0002 is strictly for DICOM files.

Before summarizing, here's a very detailed explanation of how a DICOM file actually looks like in the byte level. The screenshot bellow shows three DICOM files of exactly the same object opened in a binary editor. Each file was saved with a different transfer syntax using this simple code:

string BEEfname(filename); BEEfname+=".bee.dcm"; obj->TransferSyntax = TS_BEE; obj->saveFile(BEEfname.c_str());

string LEEfname(filename); obj->TransferSyntax = TS_LEE; LEEfname+=".lee.dcm"; obj->saveFile(LEEfname.c_str());

string LEIfname(filename); obj->TransferSyntax = TS_LEI; LEIfname+=".lei.dcm"; obj->saveFile(LEIfname.c_str());

32

Page 33: Introduction to DICOM

Up to the highlighted part, the files are identical but for the value of the transfer syntax UID element in the file meta header. You can see the 128 0's and the DICM and then the elements of group 0002. In all three files this part is little endian explicit and you can see the VR codes UL and then OB just after the preamble.

The highlighted part is the first data element of the object itself, which is element (0008,0005). While in the Little Endian files (left and right) the bytes are ordered 08 00 05 00, in the big endian file (center) the order is 00 08 00 05.

Then, in the explicit VR files (left and center) the tag is followed by 'CS' which is the VR code of this element. CS stands for Code String and tells us the data type of this element. In the implict VR file this code is missing. It is implicitly specified by the tag. Tags always have the same type (this tag is called extended character set and it is the code string of the character set encoding for the strings in the file).

After that we have the data element length which is 0xA (meaning the value is 10 bytes long) and then the value itself 'ISO_IR 192' which means that the strings in this DICOM files are encoded using UTF-8. Note that in the explicit VR files the length is stored in a two bytes while in the implicit VR file the length is stored in four bytes (quiz: though not very important, can you guess why?).

Let's summarize:

1. Serialization of DICOM objects is governed by Transfer Syntax 2. Transfer syntax sets:

o The byte order (little/big) o If VR's are serialized (explicit/implicit) o If pixel data is compressed or not (if compressed 1 is little and 2 is explicit)

3. In DICOM networking, the transfer syntax is selected per object type (SOP Class) at the negotiation phase

4. In DICOM files the transfer syntax is set in the File Meta Header (group 0002)

Recommendations as far as transfer syntax goes:

1. Always support and propose all 3 basic transfer syntaxes: LEI, LEE and BEE2. If possible, always prefer LEE as your default.

With RZDCX you are dismissed from bothering about all these details. The transformations between transfer syntaxes are taken care of internally as well as the selection of transfer syntaxes during association negotiation and when reading and writing files. The toolkit takes care of all that. You can control it when saving files as shown in the detailed example above and also compress and decompress but unless there's a very specific requirement about that in your application, you will probably never have to deal with it.

33

Page 34: Introduction to DICOM

One last comment. Many times I'm asked what transfer syntax is used by some application internally, i.e. when some application, e.g. a PACS writes files in it's internal storage, how they are stored. My answer to that is that I don't know and that you shouldn't care. Never assume anything about the internals of an application. The only thing that matters is their interfaces.

DICOM Query/Retrieve Part I

It all started when I was sitting in a cubicle with a customer, looking at the code of their workstation performing a Query/Retrieve cycle and though everything did look familiar and pretty much straight forward something bothered me.

Query/Retrieve, or Q/R for short, is the DICOM service for searching images on the PACS and getting a copy of them to the workstation where they can be displayed.

Q/R is a fundamental service and every workstation implements it. This sounds like a trivial task, just like downloading a zip file from a web site but there are a lot of details to take care of and while writing this post I realized that I will have to split it to a little sub-series. Today's post will be about the Query part and in the next post I'll get to the Retrieve.

To search the PACS we use the DICOM command C-FIND. This command takes as an argument a DICOM object that represent a query. The PACS transforms the object that we send to a query, probably to SQL, runs it and then transform every result record back into a DICOM object and send it back to us in a C-FIND response. The PACS sends one C-FIND response for every result record. While still running, the status field of the C-FIND response command is pending (0xFF00). The last response has a status success. It may of course fail and then RZDCX will throw an exception with the failure reason and status. It may also succeed but with no matches (empty results set).

Let's do some examples. This code constructs a query for searching patients:

// Fill the query object DCXOBJ obj = new DCXOBJ(); DCXELM el = new DCXELM();

el.Init((int)DICOM_TAGS_ENUM.QueryRetrieveLevel); el.Value = "PATIENT"; obj.insertElement(el);

el.Init(0x00100010); el.Value = "R*"; obj.insertElement(el);

el.Init(0x00100020); obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.PatientsSex); obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.PatientsBirthDate); obj.insertElement(el);

This code creates a DICOM object that the equivalent pseudo SQL of is:

SELECT [PATIENT NAME] , [PATIENT ID], [PATIENT SEX], [PATIENT BIRTH DATA]FROM PATIENTWHERE [PATIENT NAME] like "R%"

34

Page 35: Introduction to DICOM

Here's a study level query with explanations:// Fill the query objectDCXOBJ obj = new DCXOBJ();DCXELM el = new DCXELM();

el.Init((int)DICOM_TAGS_ENUM.studyInstanceUID); obj.insertElement(el);

SELECT [STUDY INSTANCE UID],

el.Init((int)DICOM_TAGS_ENUM.StudyDate); obj.insertElement(el);

[STUDY DATE],

el.Init((int)DICOM_TAGS_ENUM.StudyDescription); obj.insertElement(el);

[STUDY DESCRIPTION],

el.Init((int)DICOM_TAGS_ENUM.ModalitiesInStudy); obj.insertElement(el);

[MODALITIES IN STUDY]

el.Init((int)DICOM_TAGS_ENUM.QueryRetrieveLevel); el.Value = "STUDY"; obj.insertElement(el);

FROM STUDY

el.Init((int)DICOM_TAGS_ENUM.patientName); el.Value = "REIMOND^GOLDA"; obj.insertElement(el);

WHERE [PATIENT NAME] = ‘REIMOND^GOLDA’

el.Init((int)DICOM_TAGS_ENUM.patientID); el.Value = "123456789"; obj.insertElement(el);

AND

[PATIENT ID] = ‘123456789’

This code creates a DICOM object that the equivalent pseudo SQL of is:

SELECT [PATIENT NAME] , [PATIENT ID], [STUDY INSTANCE UID], [STUDY DATE], [STUDY DESCRIPTION], [MODALITIES IN STUDY]FROM STUDYWHERE [PATIENT NAME] = ‘REIMOND^GOLDA’ AND [PATIENT ID] = ‘123456789’

The analogue to SQL is very simple. Here are the rules:

1. The SELECT list, i.e. the list of ‘columns’ that we would like to get in the response is the list of all elements added to the query object.

2. The FROM table is set in the element (0008,0052) – Query Retreive Level and can be one of the following coded strings: PATIENT, STUDY, SERIES or IMAGE

3. The WHERE clause i.e. the elements that the matching is made with, is comprised of all the element that we set a value with a logical AND between them.

The WHERE clause can be further refined using some basic wildcard matching:Wildcard Meaning SQL eqivalent/Meaning* Zero or more character in string

valuesWHERE PATIENT NAME LIKE “COHEN%”

? Any Single character “COH?N” will match “COHEN” and “COHAN” and also “COH N”I don’t recommend using it.

35

Page 36: Introduction to DICOM

- For date and time attributesFROM – TO in the following formYYYYMMDD-YYYYMMDD

WHERE STUDY DATE BETWEEN 19950101 AND 20110911

YYYYMMDD- WHERE STUDY DATE >= 19950101-YYYYMMDD WHERE STUDY DATE <= 20110911

\ Value list matchingLOCALIZER\AXIAL

WHERE IMAGE TYPE in (‘LOCALIZER’, ‘AXIAL’)

There's also sequence matching that I will not explain here. and multi-value matching using the \ separator that I don't encourage you to use because my experience shows that many PACS don't implement it the way you would expect. Nevertheless, when it does work, it can be very handy. My advise, when implementing a C-FIND SCU, i.e. a PACS search client, make it as configurable as possible and give your field engineer the power to enable advanced search options for specific sites or AE titles. By default however, stick to the basics and set all the advanced options off.

When combination of date and time elements are used, the standard states that the PACS should combine them according to what one would expect so for example this:

el.Init((int)DICOM_TAGS_ENUM.StudyDate); el.Value = "19950101-20110911"; obj.insertElement(el); el.Init((int)DICOM_TAGS_ENUM.StudyTime); el.Value = "090000-170000"; obj.insertElement(el);

Should match all studies made between Jan. 1st 1995 at 9AM to Sep. 11 2011 at 5PM and not studies made from 9 to 5 in that date period. However I wouldn’t relay on that so don’t be surprised if some PACS will actually get it the second way because it was easier to implement. Open standard, did I say that already?

Continuing this line of thoughts I wouldn’t recommend having your DICOM Workstation relay on queries like this “COHEN*^A*M^MR” expecting the PACS to convert it to something like WHERE FIRST NAME LIKE ‘A%M’ AND LAST NAME LIKE ‘COHEN%’ and TITLE = ‘MR’. Stick to the basics instead.

What I would recommend is to have your workstation search screen simple as possible with the following attributes: Patient Name, Patient ID, Sex, Birth Date, Study Date and Accession Number.

If you can make the search configurable, make it as configurable as possible and also prepare configuration presets for specific PACS vendors. From personal 1st hand experience, it takes nothing more than an uncommon query attributes to crash a PACS. Try making a range matching on Body Part as a start.

Before moving forward, this is a good oportunity to explain the ^ in DOE^JOHN. Elements of type PN (VR - Value Representation, remember) i.e. that their value represent a Person Name follow a notation shared by DICOM and HL7. The ^ is called component separator and it separates the elements of the Person Name which are by order: Family Name^Given Name^Middle Name^Prefix^Suffix. Here are some examples:

Usain Bolt would be “BOLT^USAIN” President Barak Hussein Obama II would be “Obama^Barak^Hussein^President^II”

BTW, the PACS should do the string matching ignoring the case (case insensitive). Don't be surprised if you bump into one that is case sensitive.

36

Page 37: Introduction to DICOM

The query retrieve level tag (0008,0052) sets the 'table' we are selecting from. You can imagine the PACS having a database with the following hierarchical data model that we've already seen when talking about DICOM objects.

Query LevelsA good question is which are the primary keys of each level and what columns are there. Again, this is very much implementation specific and the best way to check is to read the DICOM conformance statement but the standard does provide definitions for that but, another but, it provides 3 different definitions!?:((( Don't ask why.

Originally, the DICOM standard defined three data models for the Query/Retrieve service. each data model has been assigned with one UID for the C-FIND, one UID for the C-MOVE and one UID for C-GET so all together there were 9 UIDs, 3 for search (C-FIND), 3 for download (C-MOVE) and 3 for the sync download (C-GET). C-GET got obsolete so 3 went down and then one data model called "Patient/Study Only" got obsolete too so we are now left with 'only' 4 but then some more models were added and C-GET also got back to life. Confused? don't worry, the best advise I can give you is this: Just use Patient Root because everyone supports it (I know IHE recommends Study Root).

Here are the models with their valid query levels

UID Name Query Levels

Comment

1.2.840.10008.5.1.4.1.2.1.1

Patient Root Query/Retrieve Information Model - FIND

PATIENT

STUDY

SERIES

IMAGE

Use it!

1.2.840.10008.5.1.4.1.2.1.2

Patient Root Query/Retrieve Information Model - MOVE

PATIENT

STUDY

SERIES

IMAGE

Use it!

1.2.840.10008.5.1.4.1.2.2.1

Study Root Query/Retrieve Information Model - FIND

STUDY

SERIES

IMAGE

Use it if Patient root doesn’t work for you

1.2.840.10008.5.1.4.1.2.2.2

Study Root Query/Retrieve

STUDY Use it if Patient

37

Page 38: Introduction to DICOM

Information Model - MOVE

SERIES

IMAGE

root doesn’t work for you

1.2.840.10008.5.1.4.1.2.3.1

Patient/Study Only Query/Retrieve Information Model - FIND (Retired)

PATIENT

STUDY

Don’t use

1.2.840.10008.5.1.4.1.2.3.2

Patient/Study Only Query/Retrieve Information Model - MOVE (Retired)

PATIENT

STUDY

Don’t use

What are the primary keys for each level? For patient level the key is patient id but I recommend that you always use it with combination with patient name. For all the other levels it's the UID of that level (Study Instance UID for Study, Series Instance UID for Series, SOP Instance UID for IMAGE). For Study level you can safely use Accession Number as a search key. Everyone supports it too. If you can stop at the study level, that's the best. Just download complete studies. If you have to drill down to series and image level, just don't make expectations about what the PACS is going to send you back. Each one has it's own flavor and set of supported elements. Don't get me wrong though, most PACS behave nicely but every now and then you just trip on some home brow PACS that makes you sweet.

The default implementations don't support relational queries so in order to find all the SOP Instance UID's you should first make a Study Level Query to get the Study Instance UID, then use it in a series level query to get the list of Series Instance UID's then query once for every Series Instance UID at Image level to get all the SOP instances of that series, then combine them all.

There are also counters at each level that you can use to get the number of child records:(0020,1200) Number of Patient Related Studies

(0020,1202) Number of Patient Related Series

(0020,1204) Number of Patient Related Instances

(0020,1206) Number of Study Related Series

(0020,1208) Number of Study Related Instances

(0020,1209) Number of Series Related Instances

I'm not going to list here all the mandatory and optional attributes at each level. You can check this in part 4 of the standard (section C.6). Just remember that all the elements that are marked with O are optional so not all PACS will support them. In you implementation make sure to be tolerant for not having them and relay only on the required fields.

It's time to run the query and get the results. With RZDCX you have two ways of doing it. The easy way is simply to iterate over the return value of Query. The Query methods of DCXREQ returns an Object Iterator DCXOBJIterator and you can iterate over the results like this:

// Create the requester object DCXREQ req = new DCXREQ();// send the query commandit = req.Query(LocalAEEdit.Text, TargetAEEdit.Text, HostEdit.Text,

38

Page 39: Introduction to DICOM

ushort.Parse(PortEdit.Text), "1.2.840.10008.5.1.4.1.2.1.1", obj);DCXOBJ currObj = null;try{ // Iterate over the query results for (; !it.AtEnd(); it.Next()) { currObj = it.Get(); string message = ""; DCXELM currElem = currObj.getElementByTag(0x00100020); if (currElem != null) { message += "" + currElem.Value; } currElem = currObj.getElementByTag(0x00100010); if (currElem != null) { message += " " + currElem.Value; } // … }}catch (Exception ex){ MessageBox.Show("ex.Message);}

The advantage of the code above is that it’s very simple but you have to wait until the query ends to show the results. Another way is to add a callback like this:

DCXREQ req = new DCXREQClass();req.OnQueryResponseRecieved += new IDCXREQEvents_OnQueryResponseRecievedEventHandler(QueryCallback);

And then send the query as before. The callback looks something like this:

public void QueryCallback(DCXOBJ obj){ DCXELM e = obj.getElementByTag((int)DICOM_TAGS_ENUM.patientID); // ... fill the results grid}

In this callback you can fill the results grid, update the progress button and more important stop the query if you get more results than you expected simply by throwing an application exception like the commented line in the callback above. The query response is called by RZDCX once for every C-FIND response that the called AE sends.

Lets summarize because this was a long long post:

1. A DICOM Query is represented using a DICOM Object2. Empty elements are like the select list3. Elements with value are like the where 4. Between each element there's a logical AND5. You can use * for string matching and - for date and time ranges6. The Query Level tag (0008,0052) is the FROM table

39

Page 40: Introduction to DICOM

7. The data model has 4 hierarchical levels PATIENT - STUDY - SERIES - IMAGE8. Build your Query client as tolerant as possible.

o Do not put hard constraints if some elements are missingo Use the results for display to the user and if something is missing, mark it clear in the

UI9. Do not overload functionality on the Query flow.

o Run the query, display progress bar, at the most update the results grid dynamicallyo After the query ends, over the data and see what you've got and if you can go along

with it.

Check the example QueryRetrieveSCUExample on RZDCX download page.In the next post I'll explain the retrieve part of the Q/R Service.In part I of this post, I was in a meeting with a customer reviewing their workstation code and while sitting there I was thinking to myself, why should my customers have to deal with so many details of the DICOM Q/R Service when all they really want is to retrieve a study just like they would have downloaded a zip file from a web site. And thus, later, back in my office I decided to extended the DICOM Toolkit API to include a C-MOVE method that will take care of everything including the incoming association. In today’s post I’m going to use the new MoveAndStore method to talk about the DICOM Query/Retrieve service. We’ll start at the end and then work our way backwards.

C-MOVE is a DICOM command that means this: The calling AE (we) ask the called AE (the PACS) to send all the DICOM Instances that match the identifier to the target AE. Here’s how you ask a PACS to send you the DICOM images with RZDCX (version 2.0.1.9).

public void MoveAndStore() { // Create an object with the query matching criteria (Identifier) DCXOBJ query = new DCXOBJ(); DCXELM e = new DCXELM(); e.Init((int)DICOM_TAGS_ENUM.patientName); e.Value = DOE^JOHN"; query.insertElement(e); e.Init((int)DICOM_TAGS_ENUM.patientID); e.Value = @"123456789";

query.insertElement(e); // Create an accepter to handle the incomming association

DCXACC accepter = new DCXACC(); accepter.StoreDirectory = @".\MoveAndStore";

Directory.CreateDirectory(accepter.StoreDirectory); // Create a requester and run the query

DCXREQ requester = new DCXREQ(); requester.MoveAndStore( MyAETitle, // The AE title that issue the C-MOVE IS_AE, // The PACS AE title IS_Host, // The PACS IP address IS_port, // The PACS listener port MyAETitle, // The AE title to send the query, // The matching criteria 104, // The port to receive the results accepter); // The accepter to handle the results }

Behind this rather short function hides a lot of DICOM networking and when it returns we should have all the matching objects stored in the directory “.\MoveAndStore”. Readers with some practical DICOM experience probably expect me to say that it can also fail. In that case MoveAndStore throws an

40

Page 41: Introduction to DICOM

exception with the error code and description. Sometimes you would have to set the detailed logging on and start reading logs like we did in chapter 5 of this tutorial on DICOM networking and in some later post we will look together at a DICOM log of a Q/R transaction.

The following diagram, taken from part 2 of the DICOM standard, is commonly seen in DICOM Conformance Statements as the Data Flow diagram of the Q/R Service. These diagrams and their notation are defined by the standard in part 2 that specify the DICOM Conformance Statement – a standard document that every application vendor should provide and that describes how they implemented the standard in their product. At some point we will get to how to read and write these documents.

The vertical dashed line represents the DICOM Protocol Interface between the two applications (it is usually a single dashed line but in this example it got a bit messed up). The arrows accros the interface represents DICOM associations. The arrow points from the application that initiates the association (the requester) to the application that responds to it (the responder or accepter). The upper part of the diagram shows the control chanel where the C-MOVE request is sent and statuses are reported back by the PACS. The lower part of the diagram shows the data chanel where the DICOM instances are sent to the client.

There's a lot of activity behind the scenes of this method:

1. The calling AE opens a network connection to the PACS and sends an association request with a Q/R C-MOVE presentation context. This association is like a control chanel of the operation.

2. The called AE (The PACS) examines the request and (hopefully) accepts the association and the Q/R C-MOVE presentation context and sends back an association accept primitive.

3. The calling AE sends a C-MOVE command with the identifier (the content of the query variable of our function) as a parameter. The C-MOVE command also includes the target AE Title.

4. The PACS searches its internal configuration for the target AE Title. This AE must have been previously configured by the PACS administrator because the PACS must resolve the AE Title to IP address and port number of the target AE in order to initiates an association with it.

5. The PACS transforms the identifier into a database query, runs the query on its internal database and compose a list with the matching DICOM instances.

6. The PACS starts a new association to the target AE requesting the presenation contexts of the objects it intends to send. This association is like a data chanel of the operation.

7. The PACS sends the matching instances using C-STORE commands, one C-STORE command for every matching DICOM instance.

8. While sending the C-STORE commands on the second association (the data chanel started at step 6) the PACS may send status notifications in the form of C-MOVE responses on the first association (the control chanel started at step 1) with a pending status (0xFF00) and counters of how many instances were already sent and how many are there in total.

41

Page 42: Introduction to DICOM

9. After sending all the instances the PACS closes the second association and sends a C-MOVE response with status success (or failure if something went wrong) and the C-MOVE command ends.

10. The calling AE can close the association or send another command.

Many times the target AE is the same as the calling AE (we) so we ask the PACS to send the results to us.The PACS (the called AE) who is the responder is acting as the SCP – Service Class Provider, the terms used in DICOM for a server. We (the calling AE) are the requester and are acting as the SCU – Service class user, the term used in DICOM for a client.

The target AE is acting as SCP for the C-STORE commands that the PACS sends. The new MoveAndStore method is intended solely for retreiving instances. To serve unsolisited storage commands, sometimes called DICOM push, we will use the Accepter class DCXACC that implements a DICOM server. We will see this later.

In the first part of the example above we've created a Query object. The rules for this object are almost identical to the ones we've already seen in part I when we've discussed the C-FIND command. The only difference is that we don't add empty elements because the results are DICOM instances sent to us and not records like in C-FIND.

Here are some things to remember and mistakes to avoid:

The pending C-MOVE responses are optional. The C-MOVE SCP may send pending responses while the transaction is preformed. Remember the may and don't count on these callbacks for anything important, i.e. not more then progress bar and status updates.

Some PACS will send a pending status after every instance, some will send one every 5 or 10 instances and some will send none.

Some PACS sends a success response immediately and only then start another associatin and send the resulting instances. This is not a valid implementation of DICOM but you may have to handle it.

Isolate your DICOM implementation from your application. This is true for every software. Don't mix events from the

It would have been nice to have a progress bar for the Retrieve action as well, right? Let’s add an event handler to the requester whenever a C-MOVE response is received. Here’s how:The new release of RZDCX has an extended C-MOVE callback that was missing in previous releases. Adding this callback without breaking backwards compatibility is worth a post of its own. Versions prior to 2.0.1.9 have a Boolean parameter that is true as long as the command is going on. The new build (2.0.1.9) reports the command status and the four counters for completed, remaining, failed and warning sub-operations. Sub operations are the C-STORE commands on the data channel. With this callback adding a progress bar is quite easy:

req = new DCXREQ();req.OnMoveResponseRecievedEx += new IDCXREQEvents_OnMoveResponseRecievedExEventHandler(MoveCallback);// and now call MoveAndStore just the same

The implementation of MoveCallback should look like this:

void req_OnMoveResponseRecievedEx( ushort status, ushort remaining,

42

Page 43: Introduction to DICOM

ushort completed, ushort failed, ushort warning){ // Update the progress bar and nothing more! // Throw an exception to cancel }

The callback is fired for every C-MOVE response with pending status that is sent by the SCP and again I remind you that the SCP may, meaning can but don’t have to, send pending C-MOVE responses. Some PACS will send one pending message for every C-STORE they make, others may send one every now and then and other PACS may not send pending messages at all. This means that you better avoid having important functionality coded or dependent somehow on this callback. I wouldn’t recommend anything more than a progress bar and would also have it clearly stated in the user manual that the progress bar behavior is at to the mercy of the PACS.

Sometimes you may wish to cancel the retrieve maybe because you get to many results or just the user clicked the cancel button. To do this, throw an exception (in C++ you can also return a failed HRESULT) in the callback. The toolkit will send a C-CANCEL command to the SCP on the control channel and the SCP should (hopefully) stop sending transaction.

That's it for today. In the next post we will improve our implementation by adding more event handlers and then split the Accepter from the Requester and handle the inbound association on a separate thread. This will allow us to serve all incoming associations in the same manner.

DICOM Modality Worklist

Modality worklist (MWL) is one of DICOM’s workflow services that really make a difference. It’s the difference between grocery store workflow with notes on little pieces of paper and a true modern accountable workflow.

Technically speaking, DICOM Modality Worklist is a task manager just like a piece of paper with short text and a check box or the tasks application on your iPhone (or Android). But for the imaging center or RAD department the advantages are enormous. The most obvious benefit is that there’s no need to reconcile all kind of miss spelled names in the PACS because the patient name is no longer keyed in on the modality workstation but received electronically via the MWL query. The fact that the requested procedure is also received electronically reduces the chance for doing the wrong procedure to the wrong patient. Combined with Modality Performed Procedure step (MPPS), that allows the modality to report the task status, take ownership over the task and checkmark it as done when completed, the up side is obvious. No wonder then, that many HMO’s require Modality Worklist as a mandatory feature for every imaging device they purchase.

The most basic abstraction of a task is a short description of what should be done and a checkbox. That’s all it takes. The MWL data model is a bit more complicated and has two levels.

43

Page 44: Introduction to DICOM

The top, parent, level is called “Requested Procedure” (RP) and holds the information about the patient (name, id), the study (accession number, study instance UID) and the procedure. The procedure can be described as text using attribute (0032,1060) – “Requested Procedure Description” or in a more sophisticated manner using the (0032,1064) – “Requested Procedure Code Sequence” where static tables of codes and meanings can be used to configure and maintain procedures in the RIS or HIS.The child level is called “Scheduled Procedure Step” (SPS) and holds attributes relevant to the modality and the actual procedure to be made. A single requested procedure may hold more than one SPS if the request is for a multi-modality study, for example a chest X-Ray and a CT or whatever combination, or if for example two protocols should be applied (e.g. Chest and Abdomen). As a modality, we will use the data in the RP to identify the patient and eliminate re-typing of the name and ID and the SPS to determine what exactly to do.

The DICOM images that the modality will create should use the attributes received from the MWL. When MWL is implemented, the Study Instance UID is generated in the RIS so if a multi-modality procedure is done, each modality will create a series and attach it to the Study by using the Study Instance UID received in the MWL query.The Modality Worklist Server is responsible for managing the tasks. On one hand it provides means to schedule new tasks (e.g. via HL7 or using a web form) and on the other hand it provides means to get the list of scheduled tasks (via DICOM).In this post, we’re going to write a simple MWL client using RZDCX DICOM Toolkit and discuss the details of the service and how the workflow is implemented. We’ll leave the MPPS for a future post.Let’s start with a simplified overview of the workflow at the imaging center or the radiology department.

1. An order is made for an imaging service, let’s say for example a chest X-Ray. The order can be made in various ways. For example it can arrive as a HL7 message from the HIS (Hospital Information System), or it can be that the patient walks in an the order is made at the reception desk. In either case, a new record is created in the worklist manager with the information for the service.

2. The order is scheduled and assigned to the X-Ray machine or Room that will perform the exam. If there are many X-Ray machines, the order may be assigned to one of them. The assignment is made by setting the AE title on the order and setting a date. The exact way it is done, is very much the business of the worklist manager implementation. DICOM only defines the data model entities. The relevant entity for this is scheduled procedure step. It’s an abstraction of a task with description of what should be done, when it should be done and who should do it.

3. When the X-Ray machine makes a modality worklist query, it gets the list of scheduled tasks and performs them.

Let’s have a look at a modality worklist client.

44

Page 45: Introduction to DICOM

On the upper left of this single form application you see the DICOM network parameters that we’ve already seen on chapter 5 of the DICOM tutorial when talking about DICOM networking.On the upper right side we have some filter attributes that we can use that make a lot of sense. We can filter by the AE title that the procedure was scheduled for, by the type of modality it was scheduled for and using the scheduled date. For demonstration purpose, the date matching attributes here are very detailed in order to show all possible date and time exact, open and closed range matching.The Query button packs the filter into a DICOM object and then sends it to the MWL server using the Query method of the DCXREQ interface. The SOP class for Modality Worklist is “1.2.840.10008.5.1.4.31”.The construction of the query object is a bit tricky because we have to build the parent-child hierarchy. The mechanism of making queries for hierarchical objects is called sequence matching. The server (Q/R SCP) should search all the matching Requested Procedures that have at least one child Scheduled Procedure Step with attributes that matches the query. If it finds such, the complete RP with all its child nodes is sent as a result. The client (Query SCU) may set exactly one child node in the query.

The RP and SPS entities have many attributes but for this post what’s important is to understand that we, as a modality, are performing a schedule procedure step so we are looking for the child entity. The AE title, modality and scheduled date are all attributes of the schedule procedure step so in order to perform the matching, we create a filter for the scheduled procedure step and then put it into a requested procedure object as a sequence element and this is the query we send. Here’s the code:

// Fill the query objectrp = new DCXOBJ();sps = new DCXOBJ();el = new DCXELM();

// Build the Scheduled procedure Step (SPS) itemel.Init((int)DICOM_TAGS_ENUM.ScheduledStationAETitle);sps.insertElement(el);

// A lot of code to handle all the cases of date and time matching// that eventually goes into the elements: ScheduledProcedureStepStartDate and ScheduledProcedureStepStartTimeel.Init((int)DICOM_TAGS_ENUM.ScheduledProcedureStepStartDate);…

/// This adds a filter for timeel.Init((int)DICOM_TAGS_ENUM.ScheduledProcedureStepStartTime);…

45

Page 46: Introduction to DICOM

// Handle the modality Combo Boxel.Init((int)DICOM_TAGS_ENUM.Modality);if (comboBoxModality..SelectedItem.ToString() != "Any") el.Value = comboBoxModality.SelectedItem.ToString();sps.insertElement(el);

// Now we put it as an item to sequencespsIt = new DCXOBJIterator();spsIt.Insert(sps);

// and add the sequence Scheduled Procedure Step Sequence to the requested procedure (parent) objectel.Init((int)DICOM_TAGS_ENUM.ScheduledProcedureStepSequence);el.Value = spsIt;rp.insertElement(el);

/// Add the Requested Procedure attributes that we would like to getel.Init((int)DICOM_TAGS_ENUM.RequestedProcedureID);rp.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.RequestedProcedureDescription);rp.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.studyInstanceUID);rp.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.PatientsName);rp.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.patientID);rp.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.AccessionNumber);rp.insertElement(el);

// Create the requester object and connect it's callback to our methodreq = new DCXREQClass();req.OnQueryResponseRecieved += new IDCXREQEvents_OnQueryResponseRecievedEventHandler(OnQueryResponseRecievedAction);

rp.Dump("query.txt");

// send the query commandit = req.Query(LocalAEEdit.Text, TargetAEEdit.Text, HostEdit.Text, ushort.Parse(PortEdit.Text), "1.2.840.10008.5.1.4.31", /// Modality Worklist SOP Class rp);

At the bottom of the form we have a grid that will show the results.We can handle the results either in the callback OnQueryResponseRecievedAction or iterate over the items in DCXREQ.Query return value. In this example we’ll do the later and unpack some of the attributes of the RP into a data grid. Here’s the code for it:

private void LoadResultsToGrid(DCXOBJIterator it){

46

Page 47: Introduction to DICOM

DCXOBJ currObj = null;

try { DataTable dt = new DataTable(); DataRow dr;

dt.Columns.Add(new DataColumn("Patient Name", typeof(string))); dt.Columns.Add(new DataColumn("Accession Number", typeof(string))); dt.Columns.Add(new DataColumn("Requested Procedure ID", typeof(string))); dt.Columns.Add(new DataColumn("Requested Procedure Description", typeof(string))); // Iterate over the query results for (; !it.AtEnd(); it.Next()) { currObj = it.Get(); dr = dt.NewRow(); dr["Patient Name"] = TryGetString(currObj, DICOM_TAGS_ENUM.patientName); dr["Accession Number"] = TryGetString(currObj,DICOM_TAGS_ENUM.AccessionNumber); dr["Requested Procedure ID"] = TryGetString(currObj,DICOM_TAGS_ENUM.RequestedProcedureID); dr["Requested Procedure Description"] = TryGetString(currObj,DICOM_TAGS_ENUM.RequestedProcedureDescription); dt.Rows.Add(dr); } DataView dv = new DataView(dt); dgvQueryResults.DataSource = dv; } finally { ReleaseComObject(currObj); }

}

The full source code modality worklist SCU example can be downloaded from here. This example has a master-detail data grid view that shows the SPS’s of the selected RP.

Don’t forget to download and register RZDCX (32 or 64 depending on your OS) before building the project.

Modality Performed Procedure Step

Introduction

After the post on Modality Worklist, I felt that it wouldn’t be a complete without explanation on Modality Performed Procedure Step. MWL without MPPS is like a task list without checkboxes, and after all, striking a checkbox on a completed task is great fun. Talking of which, I once red this article about productivity and task lists and since then I’m using a circular checkbox on my paper to do notes because it’s 4 times faster. Instead of 4 lines you only need one. Think of it.

IHE Comes to Rescue

Though the DICOM standard states that it doesn’t go into the details of the implementation and what

47

Page 48: Introduction to DICOM

should be the implications of MPPS on workflow it is very clear from reading the details of the standard that an MPPS is the checkmark of MWL. The gap is closed by IHE radiology technical framework that does a great job and details exactly what should be the workflow and how the implementation should look like. If you are not familiar with IHE, I strongly recommend navigating to their web site and start digging. Getting familiar with the IHE Technical Frameworks can save a lot of expensive software architect hours and more important, save you from implementing things wrong. The IHE TF is high quality software specification document that you can use almost as is for your healthcare IT software projects.

Anyway, if you don’t have time to dig inside the long documents of IHE and DICOM and HL7, here’s a short data and program flow summary:

1. The modality makes a MWL Query. Each result is a requested procedure object with one or more Scheduled Procedure Steps (SPS).

2. The user picks one SPS to perform. 3. The modality creates a new Modality Performed Procedure Step (MPPS) that references the

Study, the requested procedure, and the SPS. This is done using the N-CREATE command. 4. There’s a state machine for MPPS with three states:

1. In Progress (A dot at the center of the circular checkbox)2. Completed (A dash on the checkbox)3. Discontinued (Back to the beginning)

5. After the images acquisition is done the modality sends an updated status for the MPPS using N-SET command. The N-SET must include a performed series sequence with at least one series in it, even if the procedure was aborted (in which case the series will have no images).

6. At this point the Scheduler should dash the checkbox to mark the task as completed (or discontinued).

7. Though usually you would have a 1-to-1 relationship between a scheduled procedure and a performed procedure, the DICOM data model has a n-to-m relationship between SPS and MPPS. The connection is made by the MPPS that references the SPS that it was performed for.

The DIMSE-N Protocol

Unlike all the other command that we’ve discussed so far in this tutorial namely C-ECHO, C-STORE, C-FIND and C-MOVE that are DIMSE-C commands, MPPS uses the normalized, DIMSE-N protocol commands N-CREATE and N-SET to create and update the Modality Performed Procedure Step normalized information entity. We’ve discussed the normalized data model (aka DICOM Model of the Real World) briefly in chapter 4 when discussing DICOM Objects and stating that image objects are composites of modules from different information entities.Like, in MWL before, here’s where MPPS fits into the DICOM Data Model:

48

Page 49: Introduction to DICOM

A Study is comprised of one or more Modality Performed Procedure Steps.A Modality Performed Procedure Step includes one or more Series.(Series in turn contains one or more composite objects such as images).The complete data model can be found at the beginning of chapter 3 of the DICOM standard.What’s important to remember out of this ERD is that the MPPS is a child of the Study and parent of Series. Note that Series is still a child of a study; it is just that it can have a MPPS parent as well. If we look at workflow the sequencing is:1. An order generates a Study (thru HL7 usually).2. The Modality gets the Study Instance UID (by making a MWL query) and creates a MPPS.3. The Modality creates new Series and updates the MPPS.Practically, the Study Instance UID is created by the PACS/RIS and the Series Instance UID by the modality.

Programming a MPPS Client

49

Page 50: Introduction to DICOM

Update: Here's a link to the pre-built application. Simply download the zip file, unzip it and double click the ModalityWorklistSCU.exe. This one doesn't require any installation, not even the RZDCX.DLL because it comes with it as isolated DLL. Try it out.

Let’s do some coding now and see how it all combines together. There are already two examples of MPPS in the DICOM example applications of RZDCX, one in C++ and one in C# that you can download. These examples are for unsolicited study because the MPPS is created with no referenced SPS. In this post I want to continue the example of last post on MWL and add MPPS to it so we will start with the MWL client example.As a start we’ll add three buttons to create complete and discontinue the MPPS. We will create a MPPS for the selected SPS in the lower grid the user will do the following:1. Fill the MWL Query filters2. Click MWL Query Button3. Select an item from the upper grid holding the requested procedure4. Select an item from the lower grid holding the SPS5. Click the MPPS Start button. This will add a row to the MPPS grid at the bottom6. Select a row from the MPPS grid7. Click either Complete or AbortHere’s a screenshot of the new MWL test application after adding MPPS.

50

Page 51: Introduction to DICOM

The major changes are at the lower part of the form. The start button, the new grid with the MPPS that we already sent and the two buttons to Complete or Abort the selected MPPS.This is example has relatively a lot of code but most of it has nothing to do with DICOM. This code is rather related to the data grids and their associated data set and handling all the UI of the application. Because of that, I’ve took out all the MPPS code into a separate class (oddly named MPPS) and we’re going to go over this class now. It is really very simple and it has only two public methods, one for the N-CREATe and one for the N-SET.In the N-CREATE we create an object with the information from the RP and SPS and then send it using a N-CREATE command. If all goes well we get back from the Server the SOP Instance UID of the newly created MPPS instance.Here’s the code with lots of greens. This method create the object we are going to send with the minimum set of elements that the standard requires:

private DCXOBJ BuildNCreateObject() { DCXOBJ ssas = new DCXOBJ(); DCXELM e = new DCXELM();

51

Page 52: Introduction to DICOM

DCXUID uid = new DCXUID();

// Scheduled Step Attributes Sequence // This element hold the list of ID's that identify the SPS and RP that we // created this MPPS for

// Get the STUDT INSTANCE UID from the RP we got from the MWL // or create a new one if not found e = TryGetElement(rp, DICOM_TAGS_ENUM.studyInstanceUID); if (e != null) ssas.insertElement(e); else { // Create a new UID e.Init((int)DICOM_TAGS_ENUM.studyInstanceUID); e.Value = uid.CreateUID(UID_TYPE.UID_TYPE_STUDY); ssas.insertElement(e); }

e.Init((int)DICOM_TAGS_ENUM.ReferencedStudySequence); ssas.insertElement(e); /// Type 2 (0 length is OK)

// Get the accession number from the RP. It should always be there e = TryGetElement(rp, DICOM_TAGS_ENUM.AccessionNumber); if (e != null) ssas.insertElement(e);

// Get the RP ID and add it e = TryGetElement(rp, DICOM_TAGS_ENUM.RequestedProcedureID); if (e != null) ssas.insertElement(e);

// Get the RP description e = TryGetElement(rp, DICOM_TAGS_ENUM.RequestedProcedureDescription); if (e != null) ssas.insertElement(e);

// Get the SPS ID from the SPS object we got from the MWL e = TryGetElement(sps, DICOM_TAGS_ENUM.ScheduledProcedureStepID); if (e != null) ssas.insertElement(e);

// SPS description e = TryGetElement(sps, DICOM_TAGS_ENUM.ScheduledProcedureStepDescription); if (e != null) ssas.insertElement(e);

// If we have codes, not only text description e.Init((int)DICOM_TAGS_ENUM.ScheduledProtocolCodeSequence); ssas.insertElement(e); /// Type 2 (0 length is OK)

// Add the Scheduled Step item to a sequence DCXOBJIterator sq = new DCXOBJIterator(); sq.Insert(ssas);

e.Init((int)DICOM_TAGS_ENUM.ScheduledStepAttributesSequence); e.Value = sq;

52

Page 53: Introduction to DICOM

/// /// Performed Procedure Step Object ///

DCXOBJ pps = new DCXOBJ();

// Add the Scheduled Step sequence to the MPPS object pps.insertElement(e);

// Add Patient name e = TryGetElement(rp, DICOM_TAGS_ENUM.PatientsName); if (e != null) pps.insertElement(e);

// Add Patient ID e = TryGetElement(rp, DICOM_TAGS_ENUM.patientID); if (e != null) pps.insertElement(e);

// Add birth date null e.Init((int)DICOM_TAGS_ENUM.PatientsBirthDate); pps.insertElement(e); /// Type 2 (0 length is OK)

// Add sex null e.Init((int)DICOM_TAGS_ENUM.PatientsSex); pps.insertElement(e); /// Type 2 (0 length is OK)

// Referenced Patient Seq. e.Init((int)DICOM_TAGS_ENUM.ReferencedPatientSequence); pps.insertElement(e); /// Type 2 (0 length is OK)

// MPPS ID has not logic on it. It can be anything // The SCU have to create it but it doesn't have to be unique and the SCP // should not relay on its uniqueness // Here we use a timestamp e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepID); e.Value = DateTime.Now.ToString("yyyymmddhhmmssttt"); pps.insertElement(e);

// Performed station AE title - this identify the modality that // did the work e.Init((int)DICOM_TAGS_ENUM.PerformedStationAETitle); e.Value = aeTitle; pps.insertElement(e);

// A logical name of the station e.Init((int)DICOM_TAGS_ENUM.PerformedStationName); pps.insertElement(e); /// Type 2 (0 length is OK)

// The location e.Init((int)DICOM_TAGS_ENUM.PerformedLocation); pps.insertElement(e); /// Type 2 (0 length is OK)

// Start date and time - let's use 'Now' e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepStartDate); e.Value = DateTime.Now;

53

Page 54: Introduction to DICOM

pps.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepStartTime); e.Value = DateTime.Now; pps.insertElement(e);

// This is important! The initial state is "IN PROGRESS" e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepStatus); e.Value = "IN PROGRESS"; pps.insertElement(e); // Description, we can set it later as well in the N-SET e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepDescription); pps.insertElement(e); /// Type 2 (0 length is OK)

// Some more type 2 elements ...

e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureTypeDescription); pps.insertElement(e); /// Type 2 (0 length is OK)

e.Init((int)DICOM_TAGS_ENUM.ProcedureCodeSequence); pps.insertElement(e); /// Type 2 (0 length is OK)

e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepEndDate); pps.insertElement(e); /// Type 2 (0 length is OK)

e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepEndTime); pps.insertElement(e); /// Type 2 (0 length is OK)

// Modality - it's a type 1 e.Init((int)DICOM_TAGS_ENUM.Modality); if (modality != null && modality.Length > 0) e.Value = modality; else e.Value = "OT"; pps.insertElement(e);

// More type 2 elements e.Init((int)DICOM_TAGS_ENUM.StudyID); pps.insertElement(e); /// Type 2 (0 length is OK)

e.Init((int)DICOM_TAGS_ENUM.PerformedProtocolCodeSequence); pps.insertElement(e); /// Type 2 (0 length is OK)

e.Init((int)DICOM_TAGS_ENUM.PerformedSeriesSequence); pps.insertElement(e); /// Type 2 (0 length is OK)

return pps; }

After creating the object we can send it using DCXREQ.MPPS_Create and get the SOP Instance UID back. Note the standard also allows the SCU to create this UID. IHE set this as the SCP responsibility for MPPS.

public void Create(NetConnectionInfo connInfo) { DCXOBJ createObj = BuildNCreateObject();

54

Page 55: Introduction to DICOM

/// /// Send the N-CREATE command /// DCXREQ req = new DCXREQ(); this.SOPInstanceUID = req.MPPS_Create( connInfo.CallingAETitle, connInfo.CalledETitle, connInfo.Host, connInfo.Port, createObj); }

After this is done, we add an item to the MPPS grid that also safe keep the MPPS instance for us in this application.The next phase is done after the acquisition is done.

private DCXOBJ BuildNSetObject(bool completed) { DCXOBJ pps = new DCXOBJ(); DCXELM e = new DCXELM();

/// /// Performed Procedure Step ///

// These are the elements we can update in the N-SET

// Set the status to "COMPLETED" or "DISCONTINUED" e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepStatus); e.Value = completed ? "COMPLETED" : "DISCONTINUED"; pps.insertElement(e);

// End date and time e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepEndDate); e.Value = DateTime.Now; pps.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepEndTime); e.Value = DateTime.Now; pps.insertElement(e);

// More type 2 elements that we are allowed to change in the N-SET e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureStepDescription); pps.insertElement(e); /// Type 2 (0 length is OK)

e.Init((int)DICOM_TAGS_ENUM.PerformedProcedureTypeDescription); pps.insertElement(e); /// Type 2 (0 length is OK)

e.Init((int)DICOM_TAGS_ENUM.ProcedureCodeSequence); pps.insertElement(e); /// Type 2 (0 length is OK)

e.Init((int)DICOM_TAGS_ENUM.PerformedProtocolCodeSequence); pps.insertElement(e); /// Type 2 (0 length is OK)

55

Page 56: Introduction to DICOM

/// /// Performed Series Sequence /// - Must have at least one item even if discontinued! /// DCXOBJ series_item = new DCXOBJ(); e.Init((int)DICOM_TAGS_ENUM.PerformingPhysiciansName); series_item.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.ProtocolName); e.Value = "SOME PROTOCOL"; series_item.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.OperatorsName); series_item.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.seriesInstanceUID); e.Value = "1.2.3.4.5.6"; series_item.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.SeriesDescription); series_item.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.RetrieveAETitle); series_item.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.ReferencedImageSequence); series_item.insertElement(e);

e.Init((int)DICOM_TAGS_ENUM.ReferencedNonImageCompositeSOPInstanceSequence); series_item.insertElement(e);

DCXOBJIterator series_sq = new DCXOBJIterator(); series_sq.Insert(series_item); e.Init((int)DICOM_TAGS_ENUM.PerformedSeriesSequence); e.Value = series_sq; pps.insertElement(e);

return pps; }

To send the N-SET we have to keep the SOP Instance UID from the N-CREATE and use it. The SOP Instance UID of the MPPS is not part of the object. It is send using the Affected SOP Instance UID of the N-SET command. In the N-CREATE it is returned from the SCP in the N-CREATE response in the same manner.

public void Set(bool completed, NetConnectionInfo connInfo) { DCXOBJ ppsSet = BuildNSetObject(completed); /// /// Send the N-SET /// DCXREQ req = new DCXREQ(); req.MPPS_Set( connInfo.CallingAETitle, connInfo.CalledETitle, connInfo.Host,

56

Page 57: Introduction to DICOM

connInfo.Port, ppsSet, SOPInstanceUID); State = completed ? PPSState.COMPLETED : PPSState.DICOUNTINUED; }This reminds me, I didn't mention the SOP Class UID of the MPPS N-SET and N-CREATE command. Can you guess what it is? Surprise! It’s the MWL SOP Class UID: 1.2.840.10008.3.1.2.3.3That’s it for today. You can download the application in binary form and as source code from the following links:

DICOM Modality Performed Procedure Step Application (Application without installer) DICOM Modality Performed Procedure Step Installer Modality Performed Procedure Step Source Code RZDCX.DLL (You have to have it first in order for the applications to work)

Storage Commitment

What is DICOM Storage Commitment Service and why is it needed

Storage commitment (SCM) is a DICOM service that lets you verify if files that were previously sent to the PACS using the DICOM Storage Service were indeed stored by the application you sent it to. The SOP Class UID of this Service is “1.2.840.10008.1.20.1”. One can argue if it is necessary or not because when you send a DICOM image using C-STORE command and get a success status (0x0000) then it is supposed to be stored so the existence of Storage Commitment raises doubts about the meaning of that status in the first place. However, I can defiantly think of reasons for having such service, first because better safe than sorry and second because I already had some programming experience in the days when the DICOM standard was specified, Thanks god, we did make a long way since then. For example, some engineers, for the sake of efficiency and performance considerations, may have decided to first puts the files in a temporary storage or a queue, without even looking at their content and reply immediately with success and then later, when some batch or another thread processes the files in the queue and try to fill the database errors occur. I wouldn’t implement it this way, and I’ll give you reasons for that at the end of this post, but I did run into such implementations. The DICOM standard gives us the service but doesn’t go into the details of what is the implementation meaning should be but IHE does. IHE says that if your application creates instances and send it to somewhere, before deleting them from your local disk it should send a Storage Commitment and if all instances are OK, go ahead and make some space on your hard drive. Sounds like a good idea to me, it’s like double booking. Storage is the transaction and Storage Commitment is the reconciliation, Why not.

Storage Commitment Data Flow

All together SCM is pretty straight forward. All we need to do is to send a list of the instances and get back a reply saying which are in the PACS database and which are not, and that’s exactly how it works. Well, almost.

57

Page 58: Introduction to DICOM

The above diagram that I made hopefully explains it all. On the left there’s the SCM request. It is sent using N-ACTION command with a dataset that contains:

1. A Transaction UID, identifying this commit request2. A referenced SOP Sequence, DICOM Tag (0008,1199) with a list of SOP Class UID’s and SOP

Instance UID’s we request to commit.

During the N-ACTION all that the SCP does is to get the dataset and only say “I got it”. BTW, we need a SOP Instance UID for sending the N-ACTION because there’s an affected SOP Instance UID there (Why can’t this serve as the transaction UID? Don’t know, maybe you can answer that) so there’s a ‘well known UID 1.2.840.10008.1.20.1.1 and if you use RZDCX you shouldn’t be worried about it or about the SOP Class UID as well. On the right side of the diagram we have the result that is sent independently after the request has been received and processed. The SCP sends the Storage Commit result using N-EVENT-REPORT command that contain:

1. The transaction UID from the request.2. A list of succeeded instances

58

Page 59: Introduction to DICOM

3. If not all were ok, a list of failed instances

If the failed instances list is not empty, the storage commit result is failed. Otherwise it is succeeded. The N-EVENT-REPORT command has an Event Type ID attribute that in the case of SCM should be 1 if all instances were committed successfully or 2 if some instances failed to commit.

The Timing of the Storage Commitment Result

Getting the SCM result can sometimes be tricky. Maybe a similar design paradigm to the one described earlier led to this. Maybe, the SCP can’t answer immediately and it needs to think about it, queue the request for some batch process, check the database, compose a reply, queue it for sending and so on. Instead of just getting the results immediately in a response, DICOM lets the SCM SCP the freedom to decide when and how to answer. The SCP should send us back a N-EVENT REPORT command with the result and this result can arrive in one of three ways:

1. The SCP can Send the N-EVENT-REPORT on the same association that the SCU initiated or2. The SCP can start another association to the SCU and send the N-EVENT-REPORT, or3. The next time the SCU starts an association the SCP can send the N-EVENT-REPORT

immediately after the association negotiation phase.

The 3rd option can actually kill many DICOM Clients that implement Storage Commitment. Many implementations just don’t expect to get a command from the called AE just when they are about to send something over. This option was added to address the following scenario that allegedly can happen with mobile modalities like Cardiac Echo and are hooked in and out from the network plugs at the patient bedside. The explanation that DICOM gives for this behavior is that if for example a cardiac Doppler on a bed-side wheel is hooking in and out of the network, it might hook out before the PACS was able to initiate an association to send the result so the next time it calls in, the PACS send the result. Cleaver, we knock on the PACS door (port) and the PACS opens up and says: ‘Oh, about your last commit request, here’s the result’. Another thing, options 2 and 3 implies that the PACS should start an association with us so we have to be fully pre-configured there with AE Title, IP Address and port number.

Implementing DICOM Storage Commit SCU with RZDCX

Because the result may be coming on another association, we better have an accepter running. We don’t have to but it’s a good idea because some SCP’s will not do it any other way. In DCXACC there’s a callback named OnCommitResult that hands out the Transaction UID’s and the succeeded and failed instances lists. We can run this accepter on a different process, on a different thread or on the same thread as you’ll see in the example. To send the request you can either call CommitFiles or CommitInstances. If you call the first, RZDCX will open each file, extract the SOP Class UID and SOP Instance UID and build the request dataset. If you use CommitInstances than you have to provide the list. CommitFiles is handier though because you’re not going to delete these files before you got the commit result anyhow. These two methods will not wait for the result and hang out immediately. There’s also CommitFilesAndWaitForResult and its pair CommitInstancesAndWaitForResult that wait for a while before hanging out and gives you the same out parameters as OnCommitResult does. If your PACS support that, these would be easier. Decent PACS should have a flag that controls this behavior for every AE Title and let you select between the ways that the results are sent back to the SCU. Here’s a single threaded example. What is done here is to set an accepter and start it, then send the request, then wait for the result on the accepter. I don’t recommend doing it this way but it kind of cool as an example. The best way I think is to have the accepter run independently on another thread or process that if you implement Storage SCP (which you probably do in order to get instances back on Q/R) also handles incoming C-STORE’s.

C# Test Code

59

Page 60: Introduction to DICOM

The example code this time is directly from my nUnit test suite. You can download the sources from this link. All these examples do basically the same: Create some DICOM Test Files, Save them to disk, Send them using C-STORE, Check that they were stored with Storage Commitment and wait for the result.

public void CommitFilesSameThread() { // Create test files String fullpath = "SCMTEST"; Directory.CreateDirectory(fullpath); CommonTestUtilities.CreateDummyImages(fullpath, 1, 1);

// Send test files string MyAETitle = System.Environment.GetEnvironmentVariable("COMPUTERNAME"); DCXREQ r = new DCXREQ(); string succeededInstances; string failedInstances; r.Send(MyAETitle, IS_AE, IS_Host, IS_port, fullpath + "\\SER1\\IMG1", out succeededInstances, out failedInstances); Assert.That(failedInstances.Length == 0); Assert.That(succeededInstances.Length > 0);

// Commit files and wait for result on separate association for 30 seconds SyncAccepter a1 = new SyncAccepter(); r.CommitFiles(MyAETitle, IS_AE, IS_Host, IS_port, fullpath + "\\SER1\\IMG1"); a1.WaitForIt(30);

if (a1._gotIt) { // Check the result

Assert.True(a1._status, "Commit result is not success"); Assert.That(a1._failed_instances.Length == 0);

DCXOBJ obj = new DCXOBJ(); obj.openFile(fullpath + "\\SER1\\IMG1"); string sop_class_uid = obj.getElementByTag((int)DICOM_TAGS_ENUM.sopClassUid).Value.ToString(); string instance_uid = obj.getElementByTag((int)DICOM_TAGS_ENUM.sopInstanceUID).Value.ToString(); Assert.AreEqual(a1._succeeded_instances, sop_class_uid + ";" + instance_uid + ";"); } else Assert.Fail("Didn't get commit result");

/// Cleanup Directory.Delete(fullpath, true); }

Here’s the sync accepter: class SyncAccepter { public bool _gotIt = false; public bool _status = false; public string _transaction_uid; public string _succeeded_instances; public string _failed_instances;

60

Page 61: Introduction to DICOM

public DCXACC accepter; public string MyAETitle;

public void accepter_OnCommitResult( bool status, string transaction_uid, string succeeded_instances, string failed_instances) { _gotIt = true; _status = status; _transaction_uid = transaction_uid; _succeeded_instances = succeeded_instances; _failed_instances = failed_instances; }

public SyncAccepter() { accepter = new DCXACC(); accepter.OnCommitResult += new IDCXACCEvents_OnCommitResultEventHandler(accepter_OnCommitResult); MyAETitle = System.Environment.GetEnvironmentVariable("COMPUTERNAME"); accepter.WaitForConnection(MyAETitle, 104, 0); }

public bool WaitForIt(int timeout) { if (accepter.WaitForConnection(MyAETitle, 104, timeout)) return accepter.WaitForCommand(timeout); else return false; } }

And here’s the example that waits for the results on the same association. public void CommitFilesAndWaitForResultOnSameAssoc() { bool status = false; bool gotIt = false; String fullpath = "SCMTEST"; Directory.CreateDirectory(fullpath); CommonTestUtilities.CreateDummyImages(fullpath, 1, 1); string succeededInstances; string failedInstances; string MyAETitle = System.Environment.GetEnvironmentVariable("COMPUTERNAME"); DCXREQ r = new DCXREQ(); r.OnFileSent += new IDCXREQEvents_OnFileSentEventHandler(OnFileSent); r.Send(MyAETitle, IS_AE, IS_Host, IS_port, fullpath + "\\SER1\\IMG1", out succeededInstances, out failedInstances); DCXOBJ obj = new DCXOBJ(); obj.openFile(fullpath + "\\SER1\\IMG1"); string sop_class_uid = obj.getElementByTag((int)DICOM_TAGS_ENUM.sopClassUid).Value.ToString(); string instance_uid = obj.getElementByTag((int)DICOM_TAGS_ENUM.sopInstanceUID).Value.ToString(); string transactionUID = r.CommitFilesAndWaitForResult(MyAETitle, IS_AE, IS_Host, IS_port, fullpath + "\\SER1\\IMG1",

61

Page 62: Introduction to DICOM

5, out gotIt, out status, out succeededInstances, out failedInstances); Directory.Delete(fullpath, true);

Assert.True(status, "Commit result is not success"); Assert.That(failedInstances.Length == 0); Assert.AreEqual(succeededInstances, sop_class_uid + ";" + instance_uid + ";"); }

And here’s the common test utilities class in case you need itusing System;using System.Collections.Generic;using System.Text;using rzdcxLib;using System.IO;

namespace rzdcxNUnit{ class CommonTestUtilities { public static string TestPatientName { get { return "John^Doe"; } }

public static string TestPatientID { get { return "123765"; } }

public static string TestStudyInstanceUID { get { return "123765.1"; } }

public static string TestSeriesInstanceUID { get { return "123765.1.1"; } }

///

/// Create to series with 4 images each of test images ///

/// Root directory to put the files in /// a list of the filenames of the created images public static unsafe List<String> CreateDummyImages(String path) { return CreateDummyImages(path, 4, 2, "", false); } public static unsafe List<String> CreateDummyImages(String path, int numSeries, int numImagesPerSeries)

62

Page 63: Introduction to DICOM

{ return CreateDummyImages(path, numSeries, numImagesPerSeries, "", false); }

public static unsafe List<String> CreateDummyImages(String path, int numSeries, int numImagesPerSeries, String suffix) { return CreateDummyImages(path, numSeries, numImagesPerSeries, suffix, false); }

///

/// Create a set of test images ///

/// Root directory to put the files in /// How many series to create /// How many image files per series /// filename suffix to use /// a list of the filenames of the created images public static unsafe List<String> CreateDummyImages(String path, int numSeries, int numImagesPerSeries, String suffix, bool long_uid_names) { List<String> filesList = new List<string>(); const int ROWS = 64; const int COLUMNS = 64; const int SAMPLES_PER_PIXEL = 1; const string PHOTOMETRIC_INTERPRETATION = "MONOCHROME2"; const int BITS_ALLOCATED = 16; const int BITS_STORED = 12; const int RESCALE_INTERCEPT = 0;

DCXOBJ obj = new DCXOBJ();

/// Create an element pointer to place in the object for every tag DCXELM el = new DCXELM();

/// Set Hebrew Character Set el.Init((int)DICOM_TAGS_ENUM.SpecificCharacterSet); el.Value = "ISO_IR 192"; /// insert the element to the object obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.Rows); el.Value = ROWS; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.Columns); el.Value = COLUMNS; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.SamplesPerPixel);

63

Page 64: Introduction to DICOM

el.Value = SAMPLES_PER_PIXEL; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.PhotometricInterpretation); el.Value = PHOTOMETRIC_INTERPRETATION; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.BitsAllocated); el.Value = BITS_ALLOCATED; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.BitsStored); el.Value = BITS_STORED; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.HighBit); el.Value = BITS_STORED - 1; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.PixelRepresentation); el.Value = 0; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.WindowCenter); el.Value = (int)(1 << (BITS_STORED - 1)); obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.WindowWidth); el.Value = (int)(1 << BITS_STORED); obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.RescaleIntercept); el.Value = (short)RESCALE_INTERCEPT; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.RescaleSlope); el.Value = 1; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.GraphicData); el.Value = "456\\8934\\39843\\223\\332\\231\\100\\200\\300\\400"; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.PixelData); el.Length = ROWS * COLUMNS * SAMPLES_PER_PIXEL; el.ValueRepresentation = VR_CODE.VR_CODE_OW;

ushort[] pixels = new ushort[ROWS * COLUMNS]; for (int y = 0; y < ROWS; y++) { for (int x = 0; x < COLUMNS; x++) { int i = x + COLUMNS * y; pixels[i] = (ushort)(((i) % (1 << BITS_STORED)) - RESCALE_INTERCEPT); } } fixed (ushort* p = pixels)

64

Page 65: Introduction to DICOM

{ UIntPtr p1 = (UIntPtr)p; el.Value = p1; }

obj.insertElement(el);

// Set identifying elements el.Init((int)DICOM_TAGS_ENUM.PatientsName); el.Value = CommonTestUtilities.TestPatientName; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.patientID); el.Value = TestPatientID; obj.insertElement(el);

String study_uid = "123765.1"; el.Init((int)DICOM_TAGS_ENUM.studyInstanceUID); el.Value = CommonTestUtilities.TestStudyInstanceUID; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.StudyID); el.Value = 1; obj.insertElement(el);

el.Init((int)DICOM_TAGS_ENUM.sopClassUid); el.Value = "1.2.840.10008.5.1.4.1.1.7"; // Secondary Capture obj.insertElement(el);

for (int seriesid = 1; seriesid <= numSeries; seriesid++) {

String series_uid = study_uid + "." + seriesid; el.Init((int)DICOM_TAGS_ENUM.seriesInstanceUID); el.Value = series_uid; obj.insertElement(el);

string series_path = ""; if (long_uid_names) series_path = path + "\\" + series_uid; else series_path = path + "\\SER" + seriesid; Directory.CreateDirectory(series_path);

el.Init((int)DICOM_TAGS_ENUM.SeriesNumber); el.Value = seriesid; obj.insertElement(el);

for (int instanceid = 1; instanceid <= numImagesPerSeries; instanceid++) { String instance_uid = series_uid + "." + instanceid; el.Init((int)DICOM_TAGS_ENUM.sopInstanceUID); el.Value = instance_uid; obj.insertElement(el);

65

Page 66: Introduction to DICOM

el.Init((int)DICOM_TAGS_ENUM.InstanceNumber); el.Value = instanceid; obj.insertElement(el);

/// Save it /// String filename = ""; if(long_uid_names) filename = series_path + "\\" + instance_uid + suffix; else filename = series_path + "\\IMG" + instanceid + suffix; obj.saveFile(filename); filesList.Add(filename); } } return filesList; }

// The unsafe keyword allows pointers to be used within the following method: public static unsafe void Copy(byte* pSrc, int srcIndex, byte[] dst, int dstIndex, int count) { if (pSrc == null || srcIndex < 0 || dst == null || dstIndex < 0 || count < 0) { throw new System.ArgumentException(); }

//int srcLen = src.Length; //int dstLen = dst.Length; //if (srcLen - srcIndex < count || dstLen - dstIndex < count) //{ // throw new System.ArgumentException(); //}

// The following fixed statement pins the location of the src and dst objects // in memory so that they will not be moved by garbage collection. fixed (byte* pDst = dst) { byte* ps = pSrc; byte* pd = pDst;

// Loop over the count in blocks of 4 bytes, copying an integer (4 bytes) at a time: for (int i = 0; i < count / 4; i++) { *((int*)pd) = *((int*)ps); pd += 4; ps += 4; }

// Complete the copy by moving any bytes that weren't moved in blocks of 4: for (int i = 0; i < count % 4; i++) { *pd = *ps; pd++; ps++; } }

66

Page 67: Introduction to DICOM

} }}

Summary

Let’s wrap it all up:

1. There’s DICOM service called Storage Commitment (SCM) that gets a list of instances and gives back which are stored with our peer and can be safely deleted from our local disk and which are not and we better send them over again.

2. The SOP Class UID of Storage Commitment is 1.2.840.10008.1.20.13. The request is sent via N-ACTION with the well known SOP Instance UID4. The result is received via N-EVENT-REPORT5. The result can come on a separate association.6. If we want to give the PACS a chance to send the result on the same association, we should at

least wait for it to come for couple of seconds after sending the request.

Q&A

Q: Should the list of instances in the commit request be identical to the group of instances we sent in the association that stored the files?A: No. In DICOM there's no contextual meaning to the association.

Q: If some files failed to commit, should I send all files again or just the ones that failed?A:I would recommend sending just the one that failed.Q: What if I didn't get the result?A: Send a Storage Commit Request again.Q: How can I know the reason for the failure from the commit result?A: You can't but from the C-STORE command response you can. There's a status there and sometimes additional explanation attributes. Read the log.

Why not to implement it the way described above

One last thing I owe you. You can say that the command succeeded but the file is dead, ha? After all, we are in a hospital right, the operation was successful but the patient died? anyone? Never mind. Implementations that just store the file on disk, say OK and later parse it and find out that it’s wrong and can’t actually process it simply don’t have any way to tell the client what was the problem with the file. They lose the only chance to respond properly in the C-STORE Response. If you fear that processing the registration of a new instance during the C-STORE will delay the communication so you better review your registration process. Maybe your DICOM parser is too slow or your database connection is not optimized.

Frame 0001

67

Page 68: Introduction to DICOM

I guess that one can't escape talking about pixels when dealing with DICOM. After all, imaging is what DICOM is all about and digital images are built from pixels. So today, to celebrate release 2.0.2.6 (and the x64 version) of the DICOM Toolkit, I'm finally going to touch the heart of every DICOM Image, The Pixel Data.

For today's post I've prepared a little C++ test application that really does nothing much other then putting pixels into the pixel data of a DICOM file and save it. Well, not exactly nothing much, because it creates a huge DICOM file, more then 0.7 GB and compress it and never use more then 20 MB of memory. If you want to know how, read on.

If you haven't done so yet, download the example source code and the latest version of RZDCX and regsvr32 it. For this example, it's important to use version 2.0.2.6 or later. You can also download the 200 frames JPEG compressed DICOM file that the test application creates. Just make sure you have enough RAM before double clicking it because most viewers will take ~770MB to display it.

The first part of the application, up to line 87 (look for the comment "Setting the image pixel group elements") is rather standard. We set some mandatory elements in every DICOM object like patient name and ID and the UID's for the study series and instance and set the object class to secondary capture.

Now comes the image pixel module with the tags starting with 0028. This group is responsible for describing how to read the pixels. I'm going to go over each one and explain its use and meaning. Here's a dump of this group from the uncompressed file created by this example:

(0028,0002) US 3 # 2, 1 SamplesPerPixel(0028,0004) CS [RGB] # 4, 1 PhotometricInterpretation(0028,0006) US 0 # 2, 1 PlanarConfiguration(0028,0008) IS [200] # 4, 1 NumberOfFrames(0028,0010) US 960 # 2, 1 Rows(0028,0011) US 1280 # 2, 1 Columns(0028,0100) US 8 # 2, 1 BitsAllocated(0028,0101) US 8 # 2, 1 BitsStored(0028,0102) US 7 # 2, 1 HighBit(0028,0103) US 0 # 2, 1 PixelRepresentation(0028,1050) DS [128] # 4, 1 WindowCenter(0028,1051) DS [256] # 4, 1 WindowWidth(0028,1052) DS [0] # 2, 1 RescaleIntercept(0028,1053) DS [1] # 2, 1 RescaleSlope(7fe0,0010) OB 00\00\00\00\00\00\00\00\00\00\00\00\ ... # 500616000, 1 PixelData

And here's how the JPEG compressed dump looks like:

(0028,0002) US 3 # 2, 1 SamplesPerPixel(0028,0004) CS [YBR_FULL_422] # 12, 1 PhotometricInterpretation(0028,0006) US 0 # 2, 1 PlanarConfiguration(0028,0008) IS [200] # 4, 1 NumberOfFrames(0028,0010) US 960 # 2, 1 Rows(0028,0011) US 1280 # 2, 1 Columns(0028,0100) US 8 # 2, 1 BitsAllocated(0028,0101) US 8 # 2, 1 BitsStored(0028,0102) US 7 # 2, 1 HighBit(0028,0103) US 0 # 2, 1 PixelRepresentation(0028,2110) CS [01] # 2, 1 LossyImageCompression(0028,2112) DS [18.0721] # 8, 1 LossyImageCompressionRatio(0028,2114) CS [ISO_10918_1] # 12, 1 LossyImageCompressionMethod

68

Page 69: Introduction to DICOM

(7fe0,0010) OB (PixelSequence #=201) # u/l, 1 PixelData

You can see that there are differences because the data in these elements should describe the pixels as they are in the pixel data element. Notice the difference in Photometric Interpretation. In the JPEG compressed file, it's YBR_FULL_422 meaning the pixels are in the YCbCr color space. Also notice that the uncompressed file has a simple array of bytes in the pixel data element while the jpeg compressed has a sequence of 201 items each holding a frame and one more (the first one) with an offset table pointing to the offset to each frame in the sequence. Reading Chinese? Let's go over each element and explain them all.

Rows and Columns

Rows (0028,0010) and Columns (0028,0011) define the size of the image. Rows is the height (i.e. the Y) and Columns is the width (i.e. the X). In our example every frame is 1280 x 960 pixels. We'll see what is frame in a minute.

Samples Per Pixel

Samples per pixel (0028,0002)define the number of color channels. In grayscale images like CT and MR it is set to 1 for the single grayscale channel and for color images like in our case it is set to 3 for the three color channels Red, Green and Blue.

Photometric Interpratation

The photometric interpratation (0028,0004) element is rather unique to DICOM. It defines what does every color channel hold. You may refer it to the color space used to encode the image. In our example it is "RGB" meaning the first channel ir Red, the second is Green and the third is Blue. In grayscale images (like CT or MR) it is usually "MONOCHROME2" meaning its grayscale and 0 should be interpreted as Black. In some objects like some fluoroscopic images it may be "MONOCHROME1" meaning its grayscale and 0 should be interpreted as White. Other values may be "YBR_FULL" or "YBR_FULL_422" meaning the color channels are in the YCbCr color space that is used in JPEG.

Planar configuration

Planar configuration (0028,0006) defines how the color channels are arranged in the pixel data buffer. It is relevant only when Samples Per Pixel > 1 (i.e. for color images). It can be either 0 meaning the channels are interlaced which is the common way of serializing color pixels or 1 meaning its separated i.e. first all the reds, then all the greens and then all the blues like in print. The separated way is rather rare and when it is used its usually with RLE compression. The following image shows the two ways. BTW, If this element is missing, the default is interlaced.

69

Page 70: Introduction to DICOM

Interlaced vs separated Planar Configuration

Bits Allocated, Bits Stored and High Bit

Luckily, most toolkits and RZDCX among them take care of extracting and manipulating the pixels for you, but if you ever need to do it yourself, you'll have to do it according to these attributes and also make sure to take little/big endian into your considerations.

Bits Allocated (0028,0100) defines how much space is allocated in the buffer for every sample in bits. In our case we encode 24 bit RGB image which is the most standard image on earth so every channel is encoded in 8 bits i.e. a complete bytes so samples are always aligned with bytes. All DICOM objects (at

70

Page 71: Introduction to DICOM

least that I have looked into so far) always use complete bytes for bits allocated so it is either 8 or 16 for grayscale images with more then 256 levels of gray.

Bits Stored (0028,0101) defines how many of the bits allocated are actually used. In our case, as every sample value is between 0 and 255, all the 8 bits are used so bits stored is 8. Returning to CT images, where each sample value is between 0 and 4095, bits stored is 12 (2 power 12 is 4096). The remaining four bits are not part of the pixel value and should be masked out when reading the pixels. Sometimes these bits are used to store overlay planes data.

High Bit (0028,0102) defines how the bits stored are aligned inside the bits allocated. It is the bit number (the first bit is bit 0) of the last bit used. In the standard it is always set as one less then the bits stored but hypothetically it doesn't have to be that way. In our case, the high bit is 7. In CT it is 11. Here's an image from the DICOM standard that shows how pixels are arranged bit-wise.

CT Pixel Data in Memory

Pixel Representation

Pixel Representation (0028,0103) is either unsigned (0) or signed (1). The default is unsigned. There's an anecdotal issue here with VR codes of US and SS and this attribute because when it is set to signed then all the attributes of group 0028 should be encoded as Signed Shorts (SS) and when it's unsigned they should be unsigned (US) too.

Number of Frames

Number of Frames (0028,0008) defines how many frames are in the image. Usually there's only one and this element is omitted but in DICOM you can create multi-frame image objects and then you have to set this element. In our case we create a multi-frame image with 200 frames.

This concludes all the mandatory elements of the image pixel but for one, the pixel data.

Pixel Data

It's time to set the pixels into the pixel data element (7FE0, 0010). You may ask why all the other image pixel module elements are of group 0028 and only the pixel data is not? and though we don't ask DICOM why questions, but this time I would like to ask this question because I think there's a good answer. Think of it until we get to the end of this post.

71

Page 72: Introduction to DICOM

Lets calculate the pixel data length. We have 1280 x 960 pixels in each frame, 200 frames, 3 samples per pixel, each sample is one byte and we get:

ROWS * COLUMNS * NUMBER_OF_FRAMES * SAMPLES_PER_PIXEL * (BITS_ALLOCATED/8)

bytes, that's1280 * 960 * 200 * 3 * (8/8) = 737280000 bytes!

720MB! That's a lot! You don't expect a software to allocate such memory space in one chuck and get away with it, do you? I don't. That's why I've added the SetValueFromFile to DCXELM and SetJPEGFrames to DCXOBJ.

Let's have a look at the last part of the test application:

//////////////////////////////// // Create dummy pixels frames // ////////////////////////////////

el->Init(rzdcxLib::PixelData); el->ValueRepresentation = VR_CODE_OB;

int frameSize = ROWS*COLUMNS*SAMPLES_PER_PIXEL; char *pixels = new char[frameSize];

// Write 200 frames to a file ofstream s("pixel.data", ios_base::binary); for (int i=0; i { number2image(i+1); // Let's add some salt to it scaleImageTo(COLUMNS, ROWS, pixels); s.write(pixels, frameSize); } s.close();

delete[] pixels;

int pixelsLength = frameSize*NUMBER_OF_FRAMES; el->SetValueFromFile("pixel.data", 0, pixelsLength); obj->insertElement(el);

// Save it as is obj->saveFile("color.uncompressed.dcm");

// Compress it as JPEG Lossless obj->SaveAs("Color.jpegLossless.dcm", TS_LOSSLESS_JPEG_DEFAULT, 100, "c:\\tmp");

// Compress it as JPEG obj->SaveAs("Color.jpeg.dcm", TS_JPEG, 100, "c:\\tmp");

First we create the pixel data element and set the VR to OB. OB (other byte) means that every value in the data element is a byte and that's what we should do in this case. For CT images where every sample is stored in two bytes we should use the OW (other word) VR. Because we deal with binary data, this hint is required for the toolkit to store the data properly.

72

Page 73: Introduction to DICOM

In the for loop, we write all the frames one after the other into the pixel data file. This is where you should copy your image bytes. To add some spice to this example I've burned in the frame number on each frame. This is done using the code in DigiTools.h. It's a little something I've written for this post too. I really work on these posts.

The last step is setting the pixel data file to the pixel data element value. This call doesn't load the data into memory. You are responsible to keep the pixel data file as long as the DCXOBJ instance (obj) lives.

Now we can call saveFile. What's nice is that even here the toolkit doesn't load all the data into memory. Instead it reads small chunks of the pixels data file (16K each if I remember correctly) and copy them into the DICOM file.

If you run this application and open the windows task manager you will notice that throughout the run of this test application it never takes more then 20 MB of RAM.

The SaveAs calls at the end keeps the same standard and utilize a temp folder that you provide to do the compression. Every compressed frame is temporarily stored into a file in this folder and then the frames are copied one by one to the DICOM file.

In this example we first compress into JPEG and then to JPEG Lossless. This may take some time to run. On my workstation (Intel Core 2 Duo E6550 @ 2.33GHz with 8GB of RAM) it takes about two minutes to run, most of this time is spent on the two SaveAs calls. The toolkit is responsible for keeping the memory resources available and you are responsible to dispose the temp folder and it's content after use. I think that's nice. Try doing this with another toolkit.

If you are not keen on memory resources you can call EncodeJpeg or EncodeLosslessJpeg or simply set the TransferSyntax property of DCXOBJ. This does not require temp folder but uses a lot of memory. You can also use SetJpegFrames to set the pixel data from JPEG files and SetBMPFrames to set the pixel data from bitmap files.

So why pixel data is (7FE0,0010) and not for example (0028,9998)? I think its because it is a very long data element so we want it to be the last element in the file. As you remember, elements are written in order from small tag numbers to big tag numbers. Having the pixel data as the last element in the file, we can read all the 'DICOM header' and skip the heavy lifting of the pixel data. For example, let's say we want to scan a large data set and sort the images according to their 3D volume location and only then load the pixels into a volume buffer. In this way, we can stop reading every file after group 0028 for example and save a lot of disk time and memory. That's a good reason and good software engineering, don't you think?

DICOMDIR and Media Interchange

DICOMDIR, Have you heard this term? What does it mean? Do I need this in my system? Lots of questions. Let's try to answer some.

Here's a list with quick information about DICOMDIR:

1. Standard DICOM CD/DVD should have a file named DICOMDIR in its root directory.2. The DICOMDIR file has in it records that hold paths to DICOM files on the media.3. DICOMDIR is a DICOM Object holding a sequence of DICOMDIR records nodes each having a

type like PATIENT, STUDY, SERIES and IMAGE4. The DICOMDIR file include key attributes from the data on the media such as Patient Name,

Patient ID, Study ID, Study Date.5. The file names of DICOM files on a standard DICOM CD/DVD should be capital alphanumeric up

to 8 characters with no suffix.

73

Page 74: Introduction to DICOM

6. The CD/DVD may include other files that are not DICOM. The DICOMDIR file does not reference them.

7. The mandatory elements of the DICOMDIR nodes are not 1-2-1 with the mandatory elements in the DICOM Objects. For example Study ID which is Type 2 in DICOM Image objects is Type 1 in DICOMDIR STUDY Record. So when creating your DICOM images if you intend to create DICOMDIR for them, add these elements too.

There are two ways DICOM application can collaborate with one another. They can communicate over TCP/IP network connection or they can exchange files over some physical media.

The first figure in the DIOM standard makes sense eventuallyThe picture above, which is by the way the first figure in the DICOM standard (page 10 of chapter 1), explains that very well although when I first looked at thirteen years ago it it didn't mean anything to me.It is worth staying a bit longer on this figure because it has a lot of valuable information in it so lets work it top to bottom.

74

Page 75: Introduction to DICOM

At a first view, ignoring its content and looking only at the shape, it looks like some kind of humanoid robot with flat oval head walking on two clumsy legs made of crude blocks. Well, at least that's what I see.

The oval head that reads "Medical Information" is representing your application data.

The gray body box is your DICOM implementation (e.g. using the RZDCX toolkit) taking its data and putting it into DICOM Objects encoded properly. Then it is ready to be shared or sent to other applications, not yours, that can read the data and make useful things with it because that application too, just like your application, speaks the same language - DICOM!

Now comes the split between the two legs. The DICOM Objects can be either be written into DICOM files and then take the right side (or the left foot if this robot is facing us) through some physical media like a CD, DVD and USB, or it can take the left side and be sent over a TCP/IP network connection using DICOM commands like C-STORE.

This post is about the right side, exchanging physical media.. I've already discussed part of it, the DICOM File Meta Header, when talking about DICOM Transfer Syntax.

Let's say we got a CD from someone that tells us there's DICOM files on this CD with the information of the patient we are looking for. If we have no more information then that, we need to open the CD, look at every file on it and check if its a DICOM file, read it, figure out what's in it and decide if this is what we are looking for. Doable but slow, specially with slow media like CD, But, when this CD is made according to the standard, there's a better way. All we need to do then is to read the DICOMDIR file, find the record in it with our patient's ID and get from there the references to the DICOM files that we are looking for on the CD. The DICOMDIR is exactly what it names suggests. Its a directory record with information about DICOM files on the media. Because search actions on CD's are much slower then on a hard drive, using the DICOMDIR should theoretically shorten the time required to find and display to the user the information on the media.

With RZDCX I try to make things easier by providing the very basic API to carry out the task. Many times ease of use considerations supersede complete options and flexibility because I take the decision to implement the DICOM internals in a certain way that will be most integrative and still correct. When I take such decisions its because I assume that RZDCX users want to focus on the their strengths and count on RZDCX to do the right thing DICOM-wise. This prolog is made to explain the very thin API of the DICOMDIR Implementation class DCXDICOMDIR.

The DCXDICOMDIR class has methods to search and iterate over the different records within the DICOMDIR file. So for example to go over all the patients in the media you use the getPatientIterator method and get back an object iterator where each DCXOBJ in it is one DICOMDIR patient record.

To create a DICOMDIR use ScanAndCreate. This method scans a directory for DICOM files and writes a DICOMDIR file in its root. Before calling it, make sure your files are named properly. I use the following naming convention:

Create a directory PA001 for every patient Inside PA001 create a directory ST001 for every study Inside ST001 create a directory SE001 for every series Inside SE001 create the DICOM files and name them IM000001, IM000002 ...

Remember not to use prefixes and keep your filenames to 8 characters long, capital with alphabetic first letter. Here's a short code from the new DICOMIZER that crates a valid DICOMDIR:

private void CreateDICOMDIR(string SourceFolder) { try

75

Page 76: Introduction to DICOM

{ DCXDICOMDIRClass dir = new DCXDICOMDIRClass();

dir.ScanAndCreate(SourceFolder, APPLICATION_PROFILE.AP_GENERAL_PURPOSE, "RZ DICOMIZER", false); } catch (Exception ex) { MessageBox.Show(ex.Message); } }

Getting Oriented using the Image Plane Module

Just before diving into how to get oriented using the Image Plane Module, so we can put the letters right in our viewer, I want to get equipped with few more latin words so we understand what Radiologists are mumbling. If you're a Doctor, please be patient with us programmers.

Cuts! Three major cuts we have (or planes):

Transverse (AKA Axial) divides head from feet

Axial Cut

Sagitall Cut - right between the eyes

76

Page 77: Introduction to DICOM

Sagittal Cut

and Coronal Cut - the Filet

Coronal Cut

And now that we're done with Anatomy let's do some Geometry. In this post I'm going to start explaining the use of the Image Plane Module. To refresh on Modules read chapter 4 of the DICOM Tutorial. The Image Plane module is part of the CT Image IOD and the MR Image IOD and any other object that have a frame of reference, i.e. that has a spatial coordinates system related to the patient or in other words is a 3D scan of the body.

The Image Plane Module (in page 409 of part 3 of the standard) defines the direction of the image with respect to the patient body and mapping between the pixels in the image plane and the patient. It also gives the dimensions of the pixels (or voxels if you like) in mm. Note that with this module you can measure distances between voxels but not absolute positions because there's no anchor to the coordinate system within the patient body. Such anchor or marker is called Fiducial in image processing.

77

Page 78: Introduction to DICOM

The reminder of this post is about a single attribute of the Image Plane Module: Tag (0020,0037), Image Orientation (Patient). We'll also get to know

DICOM defines a term: "Reference Coordinates System" or RCS. The RCS is a very intuitive coordinate system of the patient body: X direction is from Right to Left. So if the patient is standing in front of you with the arm raised to the sides, then X direction is from the right hand to the left hand.

Y direction is from front to back or medical-wise from Anterior to Posterior so if the patient is standing in front of you so you see him/her from his/her left side, then Y goes from your left to your right (confusing? look at the picture).

78

Page 79: Introduction to DICOM

Z direction goes from Feet to Head. At least this is simple to explain.

Now that we know the directions, there are letters assigned to the ends of each direction:

[R] - Right - The direction in which X decreases.

[L] - Left - The direction in which X increases.

[A] - Anterior - The direction in which Y decreases.

[P] - Posterior - The direction in which Y increases.

[F] - Feet - The direction in which Z decreases.

[H] - Head - The direction in which Z increases.

Simple?! Now we can figure out the small letters on the sides of the DICOM Viewer. They can combine too. For example in this picture taken from part 17 of the standard (the explanatory part, a must read):

79

Page 80: Introduction to DICOM

Transverse Plane with patient looking at 45 deg.

This is a transverse cut with oblique patient, of course, ha?! Lets look at the letters: L is left of the patient. R is Right of the patient. P is posterior (back) and A is Anterior (Front). So this miserable patient was cut in half right through his chest (you can see the shadow of his hurt and a Thoracic Vertebrae). His feet is pointing in the direction of your nose and his head is on the other side of your screen where hopefully there's a pale Apple shaped white neon light glowing in the dark.

From some reason this patient is not in line with the image. He's Oblique to the image frame so that's more interesting because we have these two letters combinations - [PR] for posterior right and [RA] for Right Anterior.

It can get worse and we can have three letters combinations for example [PRF] for [Posterior Right Feet] if the cut was oblique too and the side where the PR is now would have been closer to your nose then the side where the AL is that would then be ALH (figure this one yourself).

The DICOM Tag (0020,0037) is "Image Orientation (Patient)". It should always have 6 values (VM = 6). It's two Normalized 3D vectors(i.e. directions). The first one we will call X and it has three components (Xx, Xy, Xz) and the second one is Y and strangely it has three components too (Yx, Yy, Yz). Xx is the projection of X on x Axis, Xy is the projection (or Cosine) of X on y Axis and so on. These are direction cosines of the image plane relative to the RCS. The first direction is the direction of the image rows in the RCS and the second direction is the direction of the image columns in the RCS.

Lets do an example. Say you got a DICOM CT Image. When you read the value of (0020,0037) good chances it will be 1\0\0\0\1\0. The X vector is (1,0,0) meaning it is exactly directed with the image pixel matrix row direction and the Y vector is (0,1,0) meaning it is exactly directed with the image pixel matrix column direction.

The following pictures explains what this means:

80

Page 81: Introduction to DICOM

What we have here is the pixel data matrix in black. On the top left is pixel (0,0) and at the bottom right, pixel (512, 512) (please forgive me that the pixels are not square. It's OK in DICOM). So That's the image. Now we have the patient coordinate system in Red. So the coordinate system of the image is exactly in the same direction of the coordinate system of the patient. The image plane is parallel to the patient Axial Plane. So now we can put the letters. R at the begging of X axis, L at the end of the X Axis, A at the beginning of Y Axis and P at the end of Y Axis.

Lets do another example and take the traverse oblique example from above.

The patient's X axis directed from right to left is (√1/2,√1/2,0) and The patient's Y axis is directed from Anterior to Posterior (front to back) is (√1/2,√1/2,0). As you can see because the RCS is a right hand side coordinates system, if you turn X right, towards Y, you tighten Z directly into the screen so the head of the patient is indeed away from you. In this example the value of (0020,0037) should be 0.707\0.707\0\0.707\0.707\0.

81

Page 82: Introduction to DICOM

Let's do the other way round. You get a DICOM file with (0020,0037) = 0.5\0\-0.8660254\0\1\0.Now you need to draw the image and put the letters on the screen. How do you do this? Here's how the image looks like when you display it on screen.

Put the right letters for (0020,0037) = 0.5\0\-0.8660254\0\1\0The First vector (0.5,0,-0.8660254) is the direction of the image rows in the RCS.

The Second vector (0,1,0) is the direction of the image columns in the RCS.

Note that 0.5 is cos(600) and -0.8660254 is cos(1500).

So if we try to draw the RCS X Direction, it is

The First vector is (0.5,0,-0.8660254) is the direction of the image rows in the patient coordinate system. 0.5 is cos(600) and -0.8660254 is cos(1500). So the image rows are rotated 600 from the patient's X direction (right-to-left) and 1500 from the patient's Z direction (feet-to-head) and the patient's Y direction is perpendicular to the image X axis.

82

Page 83: Introduction to DICOM

The second vector is (0,1,0) and is the direction of the image columns in the patient coordinate system. So the Y axis of the patient (front-to-back) is in the same direction as the Y axis of the patient. So we can now draw the projections of the patient coordinate axes on the image and mark the the letters.

We put the [A] at the negative side of Y, The [P] at the positive side of Y and so on. You can see that I've unified H and R to [HR] and F and L to [FL]. That's it.Here's the last image from the explanatory part with the image plane shown exemplified.

83