Getting started

Include files

In C++ you have to include the file imebra/imebra.h:

include <imebra/imebra.h>

In Python, import the package imebra:

from imebra.imebra import *

Loading files

Imebra can read 2 kinds of files:

  • DICOM files (via the Imebra DICOM codec)
  • Jpeg files (via the Imebra Jpeg codec)

When parsing a DICOM or a Jpeg file both the DICOM and the Jpeg codes generate an in-memory DICOM structure represented by the imebra::DataSet class (yes, also the Jpeg codec produces a DICOM structure containing an embedded JPEG file).

Imebra chooses the correct codec automatically according to the stream’s content.

To create an imebra::DataSet from a stream use the class imebra::CodecFactory.

In C++:

std::unique_ptr<imebra::DataSet> loadedDataSet(imebra::CodecFactory::load("DicomFile.dcm"));

In Python:

loadedDataSet = CodecFactory.load("DicomFile.dcm")

The previous code loads the file DicomFile.dcm.

Imebra can perform a “lazy loading”, which leaves the data on the input stream and loads it into memory only when necessary; large tags that are not needed are loaded only when necessary and then discarded from memory until they are needed once again.

This allows processing large DICOM files by loading large tags only when they are needed and is completely transparent to the client application.

To enable the “lazy loading”, specify the maximum size of the tags that must be loaded immediately. The following line leaves all the tags bigger than 2048 bytes on the stream and loads them only when needed:

Lazy loading in C++:

// Load tags in memory only if their size is equal or smaller than 2048 bytes
std::unique_ptr<imebra::DataSet> loadedDataSet(imebra::CodecFactory::load("DicomFile.dcm", 2048));

and in Python

# Load tags in memory only if their size is equal or smaller than 2048 bytes
loadedDataSet = CodecFactory.load("DicomFile.dcm", 2048)

Reading the tag’s values

Once the DataSet has been loaded your application can retrieve the tags stored in it.

Tags’ values are converted automatically between different data types unless the conversion would cause a loss of the value (e.g. the conversion of the string “10” to the number 10 succeeds, but the conversion of the string “Hello” to a number fails).

In order to retrieve a tag’s value, use one of the following methods

Alternatively, you can retrieve a imebra::ReadingDataHandler (via imebra::DataSet::getReadingDataHandler) and call the methods it offers to read the tag’s values.

If you are reading a tag containing numeric values then you can retrieve the Tag’s imebra::ReadingNumericDataHandler (via imebra::DataSet::getReadingDataHandlerNumeric) which exposes the raw memory that stores the actual data: in some cases this allow for faster information processing.

In order to identify the tag to read you must use the class imebra::TagId which takes as parameters the group ID and the tag ID or an imebra::tagId_t enumeration.

This is how you retrieve the patient’s name from the DataSet in C++:

// A patient's name can contain up to 5 values, representing different interpretations of the same name
// (e.g. alphabetic representation, ideographic representation and phonetic representation)
// Here we retrieve the first interpretations (index 0 and 1)
std::wstring patientNameCharacter = loadedDataSet->getUnicodeString(imebra::TagId(imebra::tagId_t::PatientName_0010_0010), 0);
std::wstring patientNameIdeographic = loadedDataSet->getUnicodeString(imebra::TagId(imebra::tagId_t::PatientName_0010_0010), 1);


// A patient's name can contain up to 5 values, representing different interpretations of the same name
// (e.g. alphabetic representation, ideographic representation and phonetic representation)
// Here we retrieve the first 2 interpretations (index 0 and 1)
std::wstring patientNameCharacter = loadedDataSet->getUnicodeString(imebra::TagId(0x10, 0x10), 0);
std::wstring patientNameIdeographic = loadedDataSet->getUnicode(imebra::TagId(0x10, 0x10), 1);

In python, you do it like this:

# A patient's name can contain up to 5 values, representing different interpretations of the same name
# (e.g. alphabetic representation, ideographic representation and phonetic representation)
# Here we retrieve the first 2 interpretations (index 0 and 1)
patientNameCharacter = loadedDataSet.getString(TagId(tagId_t_PatientName_0010_0010), 0)
patientNameIdeographic = loadedDataSet.getString(TagId(tagId_t_PatientName_0010_0010), 1)

Note that the previous code will throw one of the exceptions derived from imebra::MissingDataElementError if the desidered patient name component is not present in the imebra::DataSet.

You can specify a return value that is returned when the value is not present in order to avoid throwing an exception when a tag’s value cannot be found in the DataSet :

// Return an empty name if the tag is not present
std::wstring patientNameCharacter = loadedDataSet->getUnicodeString(imebra::TagId(imebra::tagId_t::PatientName_0010_0010), 0, L"");
std::wstring patientNameIdeographic = loadedDataSet->getUnicodeString(imebra::TagId(imebra::tagId_t::PatientName_0010_0010), 1, L"");


// Return an empty name if the tag is not present
std::wstring patientNameCharacter = loadedDataSet->getUnicodeString(imebra::TagId(0x10, 0x10), 0, L"");
std::wstring patientNameIdeographic = loadedDataSet->getUnicodeString(imebra::TagId(0x10, 0x10), 1, L"");

and in Python:

# Return an empty name if the tag is not present
patientNameCharacter = loadedDataSet.getString(TagId(tagId_t_PatientName_0010_0010), 0, "")
patientNameIdeographic = loadedDataSet.getString(TagId(tagId_t_PatientName_0010_0010), 1, "")

Retrieving an image

Imebra exposes two methods to retrieve images from a imebra::DataSet:

The second method applies to the image the imebra::DataSet::ModalityVOILUT transform automatically if present and is the reccommended method.

The retrieved image will have the color space & bits per channel as defined in the DataSet.

To retrieve an image in C++:

// Retrieve the first image (index = 0)
std::unique_ptr<imebra::Image> image(loadedDataSet->getImageApplyModalityTransform(0));

// Get the color space
std::string colorSpace = image->getColorSpace();

// Get the size in pixels
std::uint32_t width = image->getWidth();
std::uint32_t height = image->getHeight();

To retrieve an image in Python:

# Retrieve the first image (index = 0)
image = loadedDataSet.getImageApplyModalityTransform(0)

# Get the color space
colorSpace = image.getColorSpace()

# Get the size in pixels
width = image.getWidth()
height = image.getHeight()

In order to access the image’s pixels you can obtain a imebra::ReadingDataHandlerNumeric and then access the individual pixels via imebra::ReadingDataHandler::getSignedLong or imebra::ReadingDataHandler::getUnsignedLong. For faster processing you could also access the raw memory containing the pixels.

This is how you scan all the pixels in C++, the slow way

// let's assume that we already have the image's size in the variables width and height
// (see previous code snippet)

// Retrieve the data handler
std::unique_ptr<imebra::ReadingDataHandlerNumeric> dataHandler(image->getReadingDataHandler());

for(std::uint32 scanY(0); scanY != height; ++scanY)
    for(std::uint32 scanX(0); scanX != width; ++scanX)
        // For monochrome images
        std::int32_t luminance = dataHandler->getSignedLong(scanY * width + scanX);

        // For RGB images
        std::int32_t r = dataHandler->getSignedLong((scanY * width + scanX) * 3);
        std::int32_t g = dataHandler->getSignedLong((scanY * width + scanX) * 3 + 1);
        std::int32_t b = dataHandler->getSignedLong((scanY * width + scanX) * 3 + 2);

How to access the pixels in Python:

# let's assume that we already have the image's size in the variables width and height
# (see previous code snippet)

# Retrieve the data handler
dataHandler = image.getReadingDataHandler()

for scanY in range(0, height):
    for scanX in range(0, width):

        # For monochrome images
        luminance = dataHandler.getSignedLong(scanY * width + scanX)

        # For RGB images
        r = dataHandler.getSignedLong((scanY * width + scanX) * 3)
        g = dataHandler.getSignedLong((scanY * width + scanX) * 3 + 1)
        b = dataHandler.getSignedLong((scanY * width + scanX) * 3 + 2)

In order to make things faster you can retrieve the memory containing the data in raw format from the imebra::ReadingDataHandlerNumeric object:

// Retrieve the data handler
std::unique_ptr<imebra::ReadingDataHandlerNumeric> dataHandler(image->getReadingDataHandler());

// Get the memory pointer and the size (in bytes)
size_t dataLength;
const char* data = dataHandler->data(&dataLength);

// Get the number of bytes per each value (1, 2, or 4 for images)
size_t bytesPerValue = dataHandler->getUnitSize();

// Are the values signed?
bool bIsSigned = dataHandler->isSigned();

// Do something with the pixels...A template function would come handy

Displaying an image

An image may have to undergo several transformations before it can be displayed on a computer (or mobile) screen. Usually, the computer monitor accepts 8 bit per channel RGB (or RGBA) images, while images retrieved from a DataSet may have more than 8 bits per channel (up to 32) and may have a different color space (for instance MONOCHROME1, MONOCHROME2, YBR_FULL, etc).

While the necessary transforms are performed automatically by the imebra::DrawBitmap class, some transformations must still be performed by the client application.

In particular, the imebra::DrawBitmap class takes care of:

  • converting the color space
  • shifting the channels values to 8 bit

The client application must take care of applying the imebra::ModalityVOILUT transform (but this is easily done by calling imebra::DataSet::getImageApplyModalityTransform instead of imebra::DataSet::getImage) and the imebra::VOILUT transform.

The imebra::VOILUT can be applied only to monochromatic images and changes the image’s contrast to enhance different portions of the image (for instance just the bones or the tissue).

Usually, the dataSet contains few tags that store some pre-defined settings for the image: the client application should apply those values to the VOILUT transform. The pre-defined settings come as pairs of center/width values or as Lookup Tables stored in the DICOM sequence 0028,3010.

To retrieve the pairs center/width use the method imebra::DataSet::getVOIs, while to retrieve the LUTs use the method imebra::DataSet::getLUT.

in C++

// The transforms chain will contain all the transform that we want to
// apply to the image before displaying it
imebra::TransformsChain chain;

    // Allocate a VOILUT transform. If the DataSet does not contain any pre-defined
    //  settings then we will find the optimal ones.
    VOILUT voilutTransform;

    // Retrieve the VOIs (center/width pairs)
    imebra::vois_t vois = loadedDataSet->getVOIs();

    // Retrieve the LUTs
    std::list<std::shared_ptr<imebra::LUT> > luts;
    for(size_t scanLUTs(0); ; ++scanLUTs)
            luts.push_back(loadedDataSet->getLUT(imebra::TagId(imebra::tagId_t::VOILUTSequence_0028_3010), scanLUTs));
        catch(const imebra::MissingDataElementError&)

        voilutTransform.setCenterWidth(vois[0].center, vois[0].width);
    else if(!luts.empty())
        voilutTransform.applyOptimalVOI(image, 0, 0, width, height);


// If the image is monochromatic then now chain contains the VOILUT transform

Now we can display the image. We use imebra::DrawBitmap to obtain an RGB image ready to be displayed.

In C++

// We create a DrawBitmap that always apply the chain transform before getting the RGB image
imebra::DrawBitmap draw(chain);

// Ask for the size of the buffer (in bytes)
size_t requestedBufferSize = draw.getBitmap(image, imebra::drawBitmapType_t::drawBitmapRGBA, 4, 0, 0);

// Now we allocate the buffer and then ask DrawBitmap to fill it
std::string buffer(requestedBufferSize, char(0));
draw.getBitmap(image, imebra::drawBitmapType_t::drawBitmapRGBA, 4, &(, requestedBufferSize);

On OS-X or iOS you can use the provided method imebra::getImebraImage() to obtain a NSImage or an UIImage:

// We create a DrawBitmap that always apply the chain transform before getting the RGB image
imebra::DrawBitmap draw(chain);

// Get an NSImage (or UIImage on iOS)
NSImage* nsImage = getImebraImage(*ybrImage, draw);

Creating an empty DataSet

When creating an empty imebra::DataSet you have to specify the transfer syntax that will be used to encode it. The transfer syntax specifies also how the embedded images are compressed.

The accepted transfer syntaxes are:

  • “1.2.840.10008.1.2” (Implicit VR little endian)
  • “1.2.840.10008.1.2.1” (Explicit VR little endian)
  • “1.2.840.10008.1.2.2” (Explicit VR big endian)
  • “1.2.840.10008.1.2.5” (RLE compression)
  • “1.2.840.10008.” (Jpeg baseline 8 bit lossy)
  • “1.2.840.10008.” (Jpeg extended 12 bit lossy)
  • “1.2.840.10008.” (Jpeg lossless NH)
  • “1.2.840.10008.” (Jpeg lossless NH first order prediction)

To create an empty DataSet in C++:

// We specify the transfer syntax and the charset
imebra::DataSet dataSet("1.2.840.10008.1.2.1", "ISO 2022 IR 6");

In Python:

# We specify the transfer syntax and the charset
dataSet = DataSet("1.2.840.10008.1.2.1", "ISO 2022 IR 6")

Modifying the dataset’s content

You can set the tags values by calling the setters on the DataSet or by retrieving a WritingDataHandler for a specific tag.

WritingDataHandler objects allow modifying several tag’s buffers, while the DataSet setters allow setting only the element 0 of the first tag’s buffer.

The available DataSet setters are:

Once the DataSet has been loaded your application can retrieve the tags stored in it.

In order to write a tag’s value, use one of the following methods

The WritingDataHandler and WritingDataHandlerNumeric contain the same setters but allow to access all the tags’ elements, not just the first one.

This is how you set the patient’s name using the DataSet setter:

In C++:

dataSet.setUnicodeString(TagId(imebra::tagId_t::PatientName_0010_0010), L"Patient^Name");

In Python:

dataSet.setString(TagId(tagId_t_PatientName_0010_0010), "Patient^Name")

You can also set tags values by retrieving a WritingDataHandler and populating it: the WritingDataHandler will commit the data into the DataSet when it is destroyed:

in C++:

    std::unique_ptr<WritingDataHandler> dataHandler(dataSet.getWritingDataHandler(0));
    dataHandler->setUnicodeString(0, L"AlphabeticName");
    dataHandler->setUnicodeString(1, L"IdeographicName");
    dataHandler->setUnicodeString(2, L"PhoneticName");

    // dataHandler will go out of scope and will commit the data into the dataSet

in Python:

dataHandler = dataSet.getWritingDataHandler(0)
dataHandler.setString(0, "AlphabeticName")
dataHandler.setString(1, "IdeographicName")
dataHandler.setString(2, "PhoneticName")

# Force the commit
dataHandler = None

Embedding images into the dataSet

When an image is stored in the dataSet then it is compressed according to the dataSet’s transfer syntax.

in C++

// Create a 300 by 200 pixel image, 15 bits per color channel, RGB
imebra::Image image(300, 200, imebra::bitDepth_t::depthU16, "RGB", 15);

    std::unique_ptr<WritingDataHandlerNumeric> dataHandler(image.getWritingDataHandler());

    // Set all the pixels to red
    for(std::uint32_t scanY(0); scanY != 200; ++scanY)
        for(std::uint32_t scanX(0); scanX != 300; ++scanX)
            dataHandler->setUnsignedLong((scanY * 300 + scanX) * 3, 65535);
            dataHandler->setUnsignedLong((scanY * 300 + scanX) * 3 + 1, 0);
            dataHandler->setUnsignedLong((scanY * 300 + scanX) * 3 + 2, 0);

    // dataHandler will go out of scope and will commit the data into the image

dataSet.setImage(0, image);

in Python

# Create a 300 by 200 pixel image, 15 bits per color channel, RGB
image = Image(300, 200, bitDepth_t_depthU16, "RGB", 15)

WritingDataHandlerNumeric dataHandler = image.getWritingDataHandler();

# Set all the pixels to red
for scanY in range(0, 200):
    for scanX in range(0, 300):
        dataHandler.setUnsignedLong((scanY * 300 + scanX) * 3, 65535)
        dataHandler.setUnsignedLong((scanY * 300 + scanX) * 3 + 1, 0)
        dataHandler.setUnsignedLong((scanY * 300 + scanX) * 3 + 2, 0)

# Force the commit, don't wait for the garbage collector
dataHandler = None

dataSet.setImage(0, image);

Saving a DataSet

A DataSet can be saved using the CodecFactory:

in C++

imebra::CodecFactory::save(dataSet, "dicomFile.dcm", imebra::codecType_t::dicom);

in Python, "dicomFile.dcm", codecType_t_dicom);

Sending a DICOM command through an SCU

A SCU (Service User) acts as a client in a DICOM association (negotiated connection between 2 peers).

A DICOM association uses a TCP connection to send and receive data.

The DIMSE service (see imebra::DimseService) communicates via an association, represented either by an AssociationSCU (see imebra::AssociationSCU) or by an AssociationSCP (see imebra::AssociationSCP).

The AssociationSCU usually is the client of a DICOM service, but occasionally can act as an SCP if the SCP role for an abstractSyntax has been negotiated: this is useful to receive data via C-GET commands, where the SCP sends the requested data to the SCU via a separate C-STORE command.

The following code sends a C-STORE command to an SCP: the C-STORE command instruct the SCP to take a DICOM DataSet. In the example we prepare the separate DataSet (see imebra::DataSet) and we initialize it with the transfer syntax that we negotiated in the association.

We then send the command and wait for a response:

// Allocate a TCP stream that connects to the DICOM SCP
imebra::TCPStream tcpStream(TCPActiveAddress("", "104"));

// Allocate a stream reader and a writer that use the TCP stream.
// If you need a more complex stream (e.g. a stream that uses your
// own services to send and receive data) then use a Pipe
imebra::StreamReader readSCU(tcpStream);
imebra::StreamWriter writeSCU(tcpStream);

// Add all the abstract syntaxes and the supported transfer
// syntaxes for each abstract syntax (the pair abstract/transfer syntax is
// called "presentation context")
imebra::PresentationContext context("1.2.840.10008."); // Enhanced MR Image Storage
context.addTransferSyntax("1.2.840.10008.1.2.1"); // Explicit VR little endian
imebra::PresentationContexts presentationContexts;

// The AssociationSCU constructor will negotiate a connection through
// the readSCU and writeSCU stream reader and writer
imebra::AssociationSCU scu("SCU", "SCP", 1, 1, presentationContexts, readSCU, writeSCU, 0);

// The DIMSE service will use the negotiated association to send and receive
// DICOM commands
imebra::DimseService dimse(scu);

// Let's prepare a dataset to store on the SCP
imebra::DataSet payload(dimse.getTransferSyntax("1.2.840.10008.")); // We will use the negotiated transfer syntax
payload.setString(TagId(tagId_t::SOPInstanceUID_0008_0018), "");
payload.setString(TagId(tagId_t::SOPClassUID_0008_0016), "1.2.840.10008.");

// Fill appropriately all the DataSet tag

imebra::CStoreCommand command(
            "1.2.840.10008.", //< one of the negotiated abstract syntaxes
            payload.getString(TagId(tagId_t::SOPClassUID_0008_0016), 0),
            payload.getString(TagId(tagId_t::SOPInstanceUID_0008_0018), 0),
std::unique_ptr<imebra::DimseResponse> response(dimse.getCStoreResponse(command));

if(response->getStatus() == imebra::dimseStatus_t::success)
    // SUCCESS!

Implementign a DICOM SCP

A DICOM SCP listen for incoming connection and then communicate with the connected peer through a negotiated DICOM association.

In this example we use the imebra::TCPListener to wait for incoming connections and then negotiate the association via a AssociationSCP (see imebra::AssociationSCP).

A imebra::DimseService will be used on top of the imebra::AssociationSCP in order to receive commands and send the responses.

// Bind the port 104 to a listening socket
imebra::TCPListener tcpListener(TCPPassiveAddress("", "104"));

// Wait until a connection arrives or terminate() is called on the tcpListener
std::unique_ptr<imebra::TCPStream> tcpStream(tcpListener.waitForConnection());

// tcpStream now represents the connected socket. Allocate a stream reader and a writer
// to read and write on the connected socket
imebra::StreamReader readSCU(*tcpStream);
imebra::StreamWriter writeSCU(*tcpStream);

// Specify which presentation contexts we accept
imebra::PresentationContext context(sopClassUid);
imebra::PresentationContexts presentationContexts;

// The AssociationSCP constructor will negotiate the assocation
imebra::AssociationSCP scp("SCP", 1, 1, presentationContexts, readSCU, writeSCU, 0, 10);

// Receive commands via the dimse service
imebra::DimseService dimse(scp);

    // Receive commands until the association is closed
        // We assume we are going to receive a C-Store. Normally you should check the command type
        // (using DimseCommand::getCommandType()) and then cast to the proper class.
        std::unique_ptr<imebra::CStoreCommand> command(dynamic_cast<imebra::CStoreCommand*>(dimse.getCommand()));

        // The store command has a payload. We can do something with it, or we can
        // use the methods in CStoreCommand to get other data sent by the peer
        std::unique_ptr<imebra::DataSet> pPayload(command->getPayloadDataSet());

        // Do something with the payload

        // Send a response
        dimse.sendCommandOrResponse(CStoreResponse(*command, dimseStatusCode_t::success));
catch(const StreamEOFError&)
    // The association has been closed