Building a dicom Series

Introduction

Dicom node is a dicom in and dicom out library, however in most pipeline you will need to transform the dicom input into some other data format for processing. Then to interact with a PACS system You will need to transform data back into dicom, which this tutorial is about.

Building a series

The Blueprint.

Try and image you should build a house however, there is still many unknowns: The soil, the material, the interior and so fourth. You would start with drawing a blueprint, and as more information become available you would make the blueprint more and more concrete until you had a house.

This library is in the business of making dicom series, and there’s many unknowns, but we start by making a blueprint of series, which is found in: dicomnode.dicom.dicom_factory

from dicomnode.dicom.dicom_factory import Blueprint

blueprint = Blueprint()

At this point the blueprint is empty or the equivalent to a white paper house blueprint. We need to fill it with virtual elements which are equivalent to lines in a normal blueprint.

A virtual element describes a method to construct a single tag in a dicom series. For instance, the series description should probably indicate that this image was produces by your pipeline and the tag should be equal in all images of the dicom series. This is done with a StaticElement, because we know what should be in the tag before our pipeline is even running.

# Code continued from earlier example
from dicomnode.lib.dicom_factory import StaticElement

blueprint.add_virtual_element(
  StaticElement(0x0008103E, 'LO', 'My pipeline Series Description')
)

The arguments are:

  • The tag - that should be filled in the dicom series

  • The VR - of the tag

  • The Value - That should be filled in the tag.

A Virtual Tag Round tour

The rest of this section will is dedicated to the various types of VirtualElements. These are the building blocks of a blueprint and should be used to fill out your blueprint. Note that you should be look out for similar patterns to determine which virtual element you should use.

Series Element

With the series description we were lucky because we knew the value beforehand, but for some tags we don’t. for instance Series Instance UID. For this we need a function to produce the value, which then will be shared among all the images of the series.

from dicomnode.lib.dicom_factory import SeriesElement
from dicomnode.lib.dicom import gen_uid # Generates a UID

blueprint.add_virtual_element(
  SeriesElement(0x00200000E, 'UI' gen_uid)
)
Copy Element

In the case where you are not fabricating data, you have some input data, which is one or more dicom series. You often want to pull values from the that series. In that case you would use a CopyElement

from dicomnode.lib.dicom_factory import CopyElement

blueprint.add_virtual_element(
  CopyElement(0x00200010) # Study UID
)

However sometimes your dicom input pictures many not have this tag available and you need to decide if you should continue or abort. By default you’ll abort but you can add the optional key word if you wish to continue.

from dicomnode.lib.dicom_factory import CopyElement

blueprint.add_virtual_element(
  CopyElement(0x00081030, optional=True) # Study UID
)
Discard Element

Discard elements are element indicating that the element should be discarded. To understand why this tag is relevant. Think about all the tags in the input dicom series. If a tag isn’t present in the Blueprint, how should you proceed?

By default there’s two option either copy or discard the missing element. In the case you default to copy, but there some tags you know shouldn’t be copied. You need a discard element to express that wish.

FunctionalElement

So far all virtual element produces a value that is shared among the series. To get a different value per slice you need to use a functional element.

This function are given a InstanceEnvironment as argument which is a data class in dicomnode.lib.dicom_factory:

from dicomnode.lib.dicom_factory import FunctionalElement

@dataclass
class InstanceEnvironment:
  instance_number: int
  kwargs : Dict[Any, Any] = {}
  header_dataset: Optional[Dataset] = None
  image: Optional[Any] = None # Unmodified image
  factory: Optional['DicomFactory'] = None
  intercept: Optional[float] = None
  slope: Optional[float] = None
  scaled_image: Optional[Any] = None
  total_images: Optional[int] = None

def my_function(instance_environment: InstanceEnvironment) -> ...
  return ...

blueprint.add_virtual_element(FunctionalElement(tag, VR, my_function))
InstanceCopyElement

Not all dicom tags are the same along the series, and in the case that you need to copy the value of a varying tag. You need to use an InstanceCopyElement. A requirement to using InstanceCopyElement is that the original series has the InstanceNumber(0020,0013) tag filled! Otherwise the element is similar to a CopyElement

SequenceElement

Adding a Sequence to the produced image can be tricky. In the case where you know how many Sequence elements that should be created you can use a SequenceElement, by injecting the tag with a list of blueprint. Each blueprint corresponds to member of the Sequence.

from dicomnode.lib.dicom_factory import SequenceElement

blueprint = Blueprint()
sequence_blueprint()

Dynamic length sequences are a tag more tricky but can created using FunctionalElement’s:

from pydicom import Sequence
from random import randint

def my_sequence_generator(instance_environment: InstanceEnvironment):
  sequence = [Dataset() for _ in range(randint(1,10))]

  return Sequence(sequence)

blueprint.add_virtual_element(FunctionElement(tag,
                                              'SQ',
                                              my_sequence_generator))

You can parse user defined data in by the kwargs value when you build the series.

Factories & Default blueprints

To convert a blueprint to a real series, you use a DicomFactory from the dicomnode.dicom.dicom_factory Module. It’s a mostly stateless class with some methods of producing dicom series. Here the parent series is just the series that CopyElement will copy from.

There’s the following relevant methods:

  • build_series - This method builds a dicom series from a volume and a parent series. Note that there’s a bunch of tags, which is derived from the input image, that this function automatically sets and overwrites any virtual tags in the blueprint.

  • build_nifti_series - This method builds from a series from a nifti image. The main difference is that there’s no parent series, so if your blueprint contains a CopyElement you’ll experience an Error

  • build_series_without_image_encoding - A method that does the same as build series except it doesn’t do the image encoding, which means you have to do it

  • build_instance - This method produces a single dataset, the intent is to generate message datasets or error datasets.

  • encore_pdf - This embeds a pdf file into a dicom series, either as an embedded pdf or an image per page as a secondary capture.

These Factories have a default blueprint, which you should be included your blueprint. Either by starting out from the blueprint or combining the blueprints:


# Note this code doesn't work because DicomFactory is an abstract class
dicom_factory = DicomFactory()
# Method 1

## Building a dicom series in a pipeline


blueprint = Blueprint()
...

class MyPipeline(AbstractPipeline):
  ...
  dicom_factory = factory
  header_blueprint = blueprint

  class Processor(AbstractProcessor):
    def process(self, input_container)
      parent_series = input_container.datasets['parent']


      image = ...

      datasets = self.dicom_factory.build_series(image, blueprint, parent)

      # Do the outout