Sunteți pe pagina 1din 81

1

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 standards vocabulary is very different from the equivalent terms used in everydays
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. Its 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 vendors 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.
2

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.

Heres an example. In DICOM theres 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 theres
a different way of doing it.
Lets 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. Im
always asked why do we need three variants and I really cant 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:
"Thats the way it is in DICOM and lets just get on with it"
Chapter 3 DICOM Elements
Lets 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 dont have to talk much about the flows of this practice but for a small, one doctor
clinic, this might just work.

In this lesson, well 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. Its true that non DICOM
application can display the JPEG image just as it is now without DICOMIZING but thats another story.


We will use RZDCX DICOM Toolkit to convert the image to a DICOM object. Then well 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 well use the following three lines of C# code:
private void Convert()
{
DCXOBJ o = new DCXOBJ();
3

string jpegImage = "my_image.jpg";
o.SetJpegFrames(jpegImage);
}

Now lets 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 SequenceDelimitationItem
Lets go over this dump and see whats in it. The lines starting with a hash (#) are comments and well
ignore them.
4



DICOM Elements
A 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.
5

Tags
Every 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 well describe them in detail when we
talk about interpreting and displaying DICOM Images.
Value Representation
The 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. Well talk more about that when we discuss Implicit
and Explicit Transfer Syntax.

Value Length
Because 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 elements 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 whats 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 Object
We 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);
6

e.Value = "RZ - Software Services";
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 Objects
In chapter 3 weve 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 were 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 theres 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 (IODs) 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 chapters 3 examples by adding elements to the object until its 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


7

The data model defines Information Entities (IEs); Patient, Study, Series and Image. There are more IEs
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.
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 well get to in later chapters also makes a distinction between Normalized
and Composite operations and theres 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 IODs
Information Object Definition. IODs 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 IODs.

All DICOM Objects must include the SOP Common Module and modules from the four main IEs: 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, lets look at the SC Image IOD Modules from section A.8.1.3 of the
standard. It looks like this:

8

IE Module Reference Usage
Patient Patient C.7.1.1 M
Clinical Trial
Subject
C.7.1.3 U
Study General Study C.7.2.1 M
Patient Study C.7.2.2 U
Clinical Trial
Study
C.7.2.3 U
Series General Series C.7.3.1 M
Clinical Trial
Series
C.7.3.2 U
Equipment General
Equipment
C.7.5.1 U
SC Equipment C.8.6.1 M
Image General Image C.7.6.1 M
Image Pixel C.7.6.3 M
Device C.7.6.12 U
Specimen C.7.6.22 U
SC Image C.8.6.2 M
Overlay Plane C.9.2 U
Modality LUT C.11.1 U
VOI LUT C.11.2 U
SOP 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 Usage
Patient Patient C.7.1.1 M
Study General Study C.7.2.1 M
Series General Series C.7.3.1 M
Equipment SC Equipment C.8.6.1 M
Image General Image C.7.6.1 M
Image Pixel C.7.6.3 M
SC Image C.8.6.2 M
SOP 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 IODs. 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 dont find a
proper place for it, remember these optional modules that weve 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 well look at that later on but as already said more than
once, theres 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.
9

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 3s and also 1C
and 2Cs 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 Im going to add the C# code to add it into the object and at the end were going
to have all the code at hand.
Unique Identifiers (UIDs)
One last thing, before we dive into the code, Id 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 UIDs including the SOP Class UIDs 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 UIDs are globally Unique. Every DICOM application should acquire a root UID that is used
as a prefix to the UIDs 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 UIDs. DICOM Archives (PACS) should use the UIDs to index their databases so when
other applications make searches (Queries) they can refer to objects using the UIDs 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 well generate new UIDs 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 its 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.


10

Creating a Secondary Capture DICOM Image // create the object
DCXOBJ o = new DCXOBJ();
Table C.7-1
PATIENT MODULE ATTRIBUTES

Attribute
Name
Tag Typ
e
Attribute Description // Create element
DCXELM 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 empty
e.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-3
GENERAL STUDY MODULE ATTRIBUTES

Attribute
Name
Tag Typ
e
Attribute Description
Study
Instance UID
(0020,000
D)
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.7
e.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);
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.ReferringPhysicianNa
me);
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 ATTRIBUTES

Attribute
Name
Tag Typ
e
Attribute Description
Modality (0008,0060 1 Type of equipment // Modality is type 1 in
11

) that originally
acquired the data
used to create the
images in this Series.
See C.7.3.1.1.1 for
Defined Terms.
// 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 UID
DCXUID 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 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 = left
Note: 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.
e.Init((int)DICOM_TAGS_ENUM.Laterality);
o.insertElement(e);
12

Table C.8-24
SC EQUIPMENT MODULE ATTRIBUTES

Attribute
Name
Tag Typ
e
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 doesnt have to have a Modality tag.

Table C.7-9
GENERAL IMAGE MODULE ATTRIBUTES

Attribute
Name
Tag Typ
e
Attribute Description
Instance
Number
(0020,0013
)
2 A number that
identifies this image.
Note: This
Attribute was named
Image Number in
earlier versions of this
Standard.
e.Init((int)DICOM_TAGS_ENUM.InstanceNumber);
o.insertElement(e);
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
// Let's assume the condition
// for all the 2C's bellow
// is not met
13

otherwise. See
C.7.6.1.1.1 for further
explanation.
Note: IODs 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.
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
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-11b
IMAGE 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 on
o.SetBMPFrames("my_image.bmp");
Attribute
Name
Tag Typ
e
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

14

same number of bits
allocated. See PS 3.5
for further
explanation.
Bits Stored (0028,0101
)
1 Number of bits stored
for 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
Representatio
n
(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)
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 were going to create
here a RGB image.

Table C.8-25
SC IMAGE MODULE ATTRIBUTES

Note: all the elements in this module are type 3.
15

Table C.12-1
SOP COMMON MODULE ATTRIBUTES

Attribute
Name
Tag Typ
e
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 UID
e.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 its time to add a dump command at the end of this code and see what weve got. Heres the
complete function:


public DCXOBJ CreateSCImage()
{
// 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);
16


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();
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
17

// 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;
}

And heres 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
18

(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 weve 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.

Heres 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.

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 didnt even start talking about the
DICOM network protocol, but hay, were 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 (AEs) the nodes in the DICOM network and their name AE Title
2. Association a network peer-to-peer session between two DICOM applications
3. Association Negotiation The first part of the association in which the two AEs agree on what
can and cant be done during the Association
4. 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:
19

Complaint 20123

Burt Simpson from Springfield Memorial Hospital reports that he cant 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, whats 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 its a C library it
may be the DICOM test node, CTN. If its 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 were going to analyze together.

2011-12-1022:22:25.906000 1508 INFO Association Request Parameteres:
Our Implementation Class UID: 2.16.124.113543.6021.2
Our Implementation Version Name: RZDCX_2_0_1_8
Their Implementation Class UID:
Their Implementation Version Name:
Application Context Name: 1.2.840.10008.3.1.1.1
Calling Application Name: RZDCX
Called Application Name: PACS
Responding Application Name: resp AP Title
Our Max PDU Receive Size: 32768
Their Max PDU Receive Size: 0
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):
=LittleEndianExplicit
Requested Extended Negotiation: none
Accepted Extended Negotiation: none

2011-12-1022:22:26.062000 1508 INFO Association Request Result: Normal
Association Response Parameteres:
Our Implementation Class UID: 2.16.124.113543.6021.2
Our Implementation Version Name: RZDCX_2_0_1_8
Their Implementation Class UID: 1.2.826.0.1.3680043.2.60.0.1
Their Implementation Version Name: softlink_jdt103
Application Context Name: 1.2.840.10008.3.1.1.1
Calling Application Name: RZDCX
Called Application Name: PACS
20

Responding Application Name: PACS
Our Max PDU Receive Size: 32768
Their Max PDU Receive Size: 32768
Presentation 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: Default
Requested Extended Negotiation: none
Accepted 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 ID
Stopped 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 Ive written for this post:

public void SendSCImage(DCXOBJ o)
{
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 weve 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);
21


Before analyzing the log, lets 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 RZDCXs 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 weve 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 thats exactly whats unique in RZDCX, that you dont have to deal with all these details. Still, its
good to know whats inside so when things gets messy youll 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 its 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 thats interesting here is Association. Whats that? Thats like a network session. Its a
frame that the conversation with the called AE is going to take place in.
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 connection
2. Negotiates the association parameters to agree what can be done during the association
3. Send the DICOM object
4. Close the association
5. Close the TCP/IP connection

Lets 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.2
Our Implementation Version Name: RZDCX_2_0_1_8
Their Implementation Class UID:
Their Implementation Version Name:
22

Application Context Name: 1.2.840.10008.3.1.1.1
Calling Application Name: RZDCX
Called Application Name: PACS
Responding Application Name: resp AP Title
Our Max PDU Receive Size: 32768
Their 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. Its 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 Ive 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.2
Our Implementation Version Name: RZDCX_2_0_1_8

In this case its the RZDCX UID and version number. Its 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. Its 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. Its 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: RZDCX
Called Application Name: PACS

Note that this is just the request so its 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 doesnt 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 cant consume. Well 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
23

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

Weve 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 dont know. Thats the way it is. As I said, they are
oddly numbered.
So the first service weve 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 UIDs that are known are replaced by their name. The verification
service is a sort of high level ping. Its 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 didnt 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, Its always a good habit to
ask for the verification service. If we dont 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 weve asked for is Secondary Capture Image Storage:

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 its
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 cant 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 dont 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
24

connection by using a C-ECHO command is pure waste of time.


RZDCXs SendObject negotiates only the required SOP Class UIDs. 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
lets 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: Normal
Association Response Parameteres:
Our Implementation Class UID: 2.16.124.113543.6021.2
Our Implementation Version Name: RZDCX_2_0_1_8
Their Implementation Class UID: 1.2.826.0.1.3680043.2.60.0.1
Their Implementation Version Name: softlink_jdt103
Application Context Name: 1.2.840.10008.3.1.1.1
Calling Application Name: RZDCX
Called Application Name: PACS
Responding Application Name: PACS
Our Max PDU Receive Size: 32768
Their Max PDU Receive Size: 32768
Presentation 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: Default
Requested Extended Negotiation: none
Accepted 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 dont 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 doesnt 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 cant 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
25


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

It means that we cant store the object because the peer doesnt support this service. Yippy! We actually
could figure out whats wrong ha?! Now Burt can go to the PACS admin and ask him why his PACS cant
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 well 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, lets run this again and this time connect to port 104. Its a good idea to have the AE title, IP address
and port number of the called AE configurable in our application so we dont have to compile every
time. Most DICOM applications have such configuration. Usually its a table with at least the columns:
AE Title, host and port and maybe an id and a comment. Heres 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.2
Our Implementation Version Name: RZDCX_2_0_1_8
Their Implementation Class UID:
Their Implementation Version Name:
Application Context Name: 1.2.840.10008.3.1.1.1
Calling Application Name: RZDCX
Called Application Name: PACS
Responding Application Name: resp AP Title
Our Max PDU Receive Size: 32768
Their Max PDU Receive Size: 0
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):
=LittleEndianExplicit
Requested Extended Negotiation: none
Accepted Extended Negotiation: none

2011-12-1512:22:51.000000 4296 DEBUG Constructing Associate RQ PDU
2011-12-1512:22:51.000000 4296 DEBUG WriteToConnection, length: 310, bytes written: 310, loop
no: 1
2011-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
26

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 33
2011-12-1512:22:51.015000 4296 INFO Association Request Result: Normal
Association Response Parameteres:
Our Implementation Class UID: 2.16.124.113543.6021.2
Our Implementation Version Name: RZDCX_2_0_1_8
Their Implementation Class UID: 1.2.826.0.1.3680043.2.60.0.1
Their Implementation Version Name: softlink_jdt103
Application Context Name: 1.2.840.10008.3.1.1.1
Calling Application Name: RZDCX
Called Application Name: PACS
Responding Application Name: PACS
Our Max PDU Receive Size: 32768
Their Max PDU Receive Size: 32768
Presentation 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: =LittleEndianExplicit
Requested Extended Negotiation: none
Accepted Extended Negotiation: none

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 bytes
2011-12-1512:22:51.031000 4296 DEBUG WriteToConnection, length: 12, bytes written: 12, loop no:
1
2011-12-1512:22:51.031000 4296 DEBUG WriteToConnection, length: 146, bytes written: 146, loop
no: 1
2011-12-1512:22:51.031000 4296 DEBUG DIMSE sendDcmDataset: sending 7894 bytes
2011-12-1512:22:51.031000 4296 DEBUG WriteToConnection, length: 12, bytes written: 12, loop no:
1
2011-12-1512:22:51.031000 4296 DEBUG WriteToConnection, length: 7894, bytes written: 7894, loop
no: 1
27

2011-12-1512:22:51.046000 4296 INFO DIMSE receiveCommand
2011-12-1512:22:51.062000 4296 INFO DIMSE receiveCommand: 1 pdv's (178 bytes), presID=3
2011-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:
1
Stopped 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 Ill leave transfer
syntaxes for another time.

Lets summarize what weve 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 AEs i.e. it is peer-to-peer.
3. The DICOM session is called Association
4. The association is divided into two stages. The first stage is called Association Negotiation. In
the second stage the two AEs 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
marked with which services it accepted and can be used and which it declined and cant 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.
Thats 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
28

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
=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.
29

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 endian
3) CT Image storage, jpeg lossless
In 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
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
30

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());




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.

31


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 BEE
2. 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.

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,
32

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 PATIENT
WHERE [PATIENT NAME] like "R%"

Here's a study level query with explanations:
// Fill the query object
DCXOBJ 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
33

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 STUDY
WHERE *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
values
WHERE PATIENT NAME LIKE COHEN%

? Any Single character COH?N will match COHEN and COHAN
and also COH N
I dont recommend using it.
- For date and time attributes
FROM TO in the following form
YYYYMMDD-YYYYMMDD
WHERE STUDY DATE BETWEEN 19950101 AND
20110911
YYYYMMDD- WHERE STUDY DATE >= 19950101
-YYYYMMDD WHERE STUDY DATE <= 20110911
\ Value list matching
LOCALIZER\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";
34

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 wouldnt relay on that so dont 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 wouldnt 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.

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 Levels
A 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
35

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
doesnt
work for
you
1.2.840.10008.5.1.4.1.2.2.2 Study Root
Query/Retrieve
Information
Model - MOVE
STUDY
SERIES
IMAGE
Use it if
Patient
root
doesnt
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
Dont use
1.2.840.10008.5.1.4.1.2.3.2 Patient/Study
Only
Query/Retrieve
Information
Model - MOVE
(Retired)
PATIENT
STUDY
Dont 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.

36

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 command
it = req.Query(LocalAEEdit.Text,
TargetAEEdit.Text,
HostEdit.Text,
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);
37

}

The advantage of the code above is that its 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 Object
2. Empty elements are like the select list
3. Elements with value are like the where
4. Between each element there's a logical AND
5. You can use * for string matching and - for date and time ranges
6. The Query Level tag (0008,0052) is the FROM table
7. The data model has 4 hierarchical levels PATIENT - STUDY - SERIES - IMAGE
8. Build your Query client as tolerant as possible.
o Do not put hard constraints if some elements are missing
o Use the results for display to the user and if something is missing, mark it clear in the
UI
9. Do not overload functionality on the Query flow.
o Run the query, display progress bar, at the most update the results grid dynamically
o 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
todays post Im going to use the new MoveAndStore method to talk about the DICOM Query/Retrieve
service. Well 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.
Heres how you ask a PACS to send you the DICOM images with RZDCX (version 2.0.1.9).

public void MoveAndStore()
{
38

// 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
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.





39

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.
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.
40

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? Lets add an event
handler to the requester whenever a C-MOVE response is received. Heres 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,
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 dont 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 wouldnt 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
41

Modality worklist (MWL) is one of DICOMs workflow services that really make a difference. Its 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 theres 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 HMOs 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. Thats
all it takes. The MWL data model is a bit more complicated and has two levels.

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, were going to write a simple MWL client using RZDCX DICOM Toolkit and discuss the details
of the service and how the workflow is implemented. Well leave the MPPS for a future post.
42

Lets 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, lets 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. Its 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.
Lets have a look at a modality worklist client.


On the upper left of this single form application you see the DICOM network parameters that weve
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 whats 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. Heres the code:

// Fill the query object
43

rp = new DCXOBJ();
sps = new DCXOBJ();
el = new DCXELM();

// Build the Scheduled procedure Step (SPS) item
el.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
ScheduledProcedureStepStartTime
el.Init((int)DICOM_TAGS_ENUM.ScheduledProcedureStepStartDate);


/// This adds a filter for time
el.Init((int)DICOM_TAGS_ENUM.ScheduledProcedureStepStartTime);


// Handle the modality Combo Box
el.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 sequence
spsIt = new DCXOBJIterator();
spsIt.Insert(sps);

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

/// Add the Requested Procedure attributes that we would like to get
el.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 method
req = new DCXREQClass();
req.OnQueryResponseRecieved += new
IDCXREQEvents_OnQueryResponseRecievedEventHandler(OnQueryResponseRecievedAction);
44


rp.Dump("query.txt");

// send the query command
it = 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 well do the later and unpack some of the
attributes of the RP into a data grid. Heres the code for it:

private void LoadResultsToGrid(DCXOBJIterator it)
{
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 SPSs of the selected RP.

45

Dont 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 wouldnt 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 Im using a circular checkbox on my paper to do notes because
its 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 doesnt go into the details of the implementation and what
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 dont have time to dig inside the long documents of IHE and DICOM and HL7, heres 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. Theres 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 weve 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
46

commands N-CREATE and N-SET to create and update the Modality Performed Procedure Step
normalized information entity. Weve 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, heres where MPPS fits into the DICOM Data Model:

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.
Whats 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.
47

Practically, the Study Instance UID is created by the PACS/RIS and the Series Instance UID by the
modality.

Programming a MPPS Client
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.

Lets 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 well 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 filters
2. Click MWL Query Button
3. Select an item from the upper grid holding the requested procedure
4. Select an item from the lower grid holding the SPS
5. Click the MPPS Start button. This will add a row to the MPPS grid at the bottom
6. Select a row from the MPPS grid
7. Click either Complete or Abort
Heres a screenshot of the new MWL test application after adding MPPS.

48




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, Ive took out all the MPPS code into a separate class (oddly named MPPS) and were
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.
Heres 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();
49

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;
50


///
/// 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;
51

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();
52

///
/// 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)
53


///
/// 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,
54

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! Its the MWL SOP Class UID: 1.2.840.10008.3.1.2.3.3
Thats 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 wouldnt implement it this way, and Ill 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
doesnt 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, its 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 thats exactly how it works.
Well, almost.
55




The above diagram that I made hopefully explains it all. On the left theres the SCM request. It is sent
using N-ACTION command with a dataset that contains:
1. A Transaction UID, identifying this commit request
2. A referenced SOP Sequence, DICOM Tag (0008,1199) with a list of SOP Class UIDs and SOP
Instance UIDs 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 theres an affected SOP Instance UID there (Why
cant this serve as the transaction UID? Dont know, maybe you can answer that) so theres a well
known UID 1.2.840.10008.1.20.1.1 and if you use RZDCX you shouldnt 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
56

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 cant 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 or
2. The SCP can start another association to the SCU and send the N-EVENT-REPORT, or
3. The next time the SCU starts an association the SCP can send the N-EVENT-REPORT
immediately after the association negotiation phase.
The 3
rd
option can actually kill many DICOM Clients that implement Storage Commitment. Many
implementations just dont 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, heres 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
dont have to but its a good idea because some SCPs will not do it any other way. In DCXACC theres a
callback named OnCommitResult that hands out the Transaction UIDs 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 youll 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 youre 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.
Theres 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. Heres 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 dont 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-STOREs.
C# Test Code
57

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);
}

Heres 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;
58

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 heres 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",
59

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 heres the common test utilities class in case you need it
using 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)
60

{
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);
61

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)
62

{
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);

63

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++;
}
}
64

}
}
}

Summary
Lets wrap it all up:
1. Theres 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.1
3. The request is sent via N-ACTION with the well known SOP Instance UID
4. The result is received via N-EVENT-REPORT
5. 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 its wrong and
cant actually process it simply dont 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
65

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
66

(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.
67


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
68

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.

69

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's
1280 * 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.

70

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 IMAGE
4. 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.
71

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 eventually
The 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.

72

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)
{
73

try
{
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
74


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.
75







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).
76


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):
77


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:
78


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.
79





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\0
The 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(60
0
) and -0.8660254 is cos(150
0
).

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(60
0
) and -0.8660254 is cos(150
0
). So the image rows are rotated 60
0
from the patient's X
direction (right-to-left) and 150
0
from the patient's Z direction (feet-to-head) and the patient's Y
direction is perpendicular to the image X axis.
80



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.
81

S-ar putea să vă placă și