DICOM Routing using Capacitor

Conditional routing and compression to multiple PACS.

DICOM Capacitor is a store-and-forward DICOM router, and conditional routing is among the simplest of its many features.

Here, we show an example of a configuration that makes Capacitor:

  • Receive studies from any modality,
  • Compress images on the way to a primary PACS, and
  • Re-route DICOM SR instances to a secondary PACS.

Getting Started

To begin, collect information about our PACS’.

For this example, we have gathered the following:

AE TitleIP AddressPortPreferred Transfer Syntax
Primary PACSIMAGEPACS192.168.1.200104JPEG 2000 Lossless
Secondary PACSREPORTPACS192.168.1.201104Any

We’re ready to install.

Install Capacitor

  1. Click this link to download the latest evaluation of DICOM Capacitor:
    https://store.fluxinc.ca/files/DCP/latest
  2. Run the installer and complete the installation process.
  3. Once finished, open DICOM Capacitor Control using your start menu, and, if needed, follow the prompts to obtain an evaluation license.
  4. Start, and then stop, the “DICOM Capacitor” service using either the Control applet, the Windows Services snap-in, or by running the following commands:
$ net start DICOMCapacitorService
$ net stop DICOMCapacitorService

Close DICOM Capacitor Control if it’s still open because all further steps will be performed outside the user interface.

Configure Capacitor

All Capacitor configuration files live in the %ProgramData%\Flux Inc\DICOM Capacitor folder.

Here are the three files we will be editing:

  • config.yml, which defines Capacitor’s operational settings, and
  • nodes.yml, which defines all destination (and source) nodes
  • routings.yml, which defines the routing rules

Set Capacitor Defaults

  1. Open config.yml in your YAML text editor — we recommend Atom, or Visual Studio Code.
  2. Confirm the value of scpPort:

    scpPort: 1040

    This is the port on which Capacitor will accept C-STORE (and C-ECHO) associations.
  3. Turn on the routing module by adding “route” the filters setting:

    filters: route

3. Finally, save and close config.yml.

Define the Two Nodes

Next, we define our destination nodes.

  1. First, open nodes.yml in your YAML text editor.
  2. Replace the contents of nodes.yml with:
# Primary PACS
- NodeRole: Storage
  AeTitle: IMAGEPACS
  HostName: 192.168.1.200
  Impersonate: true
  Port: 104
  TransferSyntax: JPEG2000Lossless
  MinimumLineSpeed: 500

# Secondary PACS
- NodeRole: Storage
  AeTitle: REPORTPACS
  HostName: 192.168.1.201
  Impersonate: true
  Port: 104
  MinimumLineSpeed: 500

According to this node definition file, Capacitor will:

  • Listen for C-ECHO and C-STORE associations on port 1040.
  • Respond and automatically route to AE titles IMAGEPACS and REPORTPACS1.
  • Compress all instances bound for IMAGEPACS to JPEG 2000 Lossless.
  • Impersonate, i.e., use the sending modality’s own AE title, when forwarding to these destinations.

1 Capacitor will also respond to DCP_IMAGEPACS, and DCP_REPORTPACS in the same way.

All that remains is for us to configure our routing rules.

Define DICOM Routing Rules

All route definitions live in a file called routings.yml.

  1. Create routings.yml if it does not already exist, and open it in your YAML editor.
  2. Paste in the following content and save the file:
---
- AeTitles:
    - IMAGEPACS
  Conditions:
    - Tag: 0008,0016
      MatchExpression: ^1\.2\.840\.10008\.5\.1\.4\.1\.1\.88\.[\d]{1,2}$
  Actions:
    - Description: Re-route to REPORTPACS
      Type: add_destination
      Target: REPORTPACS
      RemoveOriginal: true

This definition tells Capacitor’s routing module to:

  • Consider only instances bound for IMAGEPACS
  • Determine if the SOP Class UID (0008,0016) matches the regular expression ^1.2.840.10008.5.1.4.1.1.88.[\d]{1,2}$, which matches all DICOM SR types
  • If so, then add the destination REPORTPACS, which is defined in our nodes.yml file, and
  • Remove the original route for this instance

We’re ready to test.

Start Capacitor and Inspect the Logs

  1. Start the DICOM Capacitor service using the Windows Services snap-in, or:

    $ net start DICOMCapacitorService
  2. Next, open and inspect
    %ProgramData%\Flux Inc\DICOM Capacitor\logs\capacitor_service.log

The last few lines in this log should confirm that Capacitor is running and accepting connections on behalf of our two nodes.


Comment below with any questions, and don’t hesitate to reach out for our help!

Multiple PACS and Print Destinations using DICOM Printer 2

This post describes how to configure DP2 for multiple PACS and Print destinations. At the completion of these steps you will be able to:

  • Print a document or image to DICOM Printer 2,
  • Choose from among three destinations (2 PACS systems, and 1 Dry Imager), and
  • Have DICOM Printer deliver based on your choices.

Note:
DICOM Printer is a print-to-PACS product. If you’re looking for a store-forward multi-destination PACS router, check out Capacitor.

Getting Started

Before we continue, you might want to read our previous post about automating storage to PACS; it will familiarize you with basic DP2 configuration, <ActionList> components, as well as the <Workflow> block.

We’ll begin this document with the configuration from the other post:

<DicomPrinterConfig>
  ...
  <ActionsList>
    <ParseJobTextFile name="ParseContents">
      <DcmTag tag="(8,50)">^(.*)\-.*$</DcmTag>
      <DcmTag tag="(8,60)">^.*\-(.*)$</DcmTag>
    </ParseJobFileName>
    <Query name="Query" type="Worklist">
      <DcmTag tag="(0008,0050)" />
      <DcmTag tag="(0008,0060)" />
      <DcmTag tag="(0010,0020)" />
      <DcmTag tag="(0020,000D)" />
      <ConnectionParameters>
        <MyAeTitle>DICOMPRINTER2</MyAeTitle>
        <PeerAeTitle>DVT</PeerAeTitle>
        <Host>localhost</Host>
        <Port>104</Port>
      </ConnectionParameters>
    </Query>
    <Store name="Store">
      <Compression type="RLE" />
      <ConnectionParameters>
        <PeerAeTitle>CONQUESTSRV1</PeerAeTitle>
        <Host>192.168.0.41</Host>
        <Port>5678</Port>
      </ConnectionParameters>
    </Store>
  </ActionsList>
  <Workflow>
    <Perform action="ParseContents" onError="Hold" />
    <Perform action="Query" onError="Suspend" />
    <If field="QUERY_FOUND" value="1">
      <Statements>
        <Perform action="Store" />
      </Statements>
      <Else>
        <Suspend resumeAction="Query" />
      </Else>
    </If>
  </Workflow>
</DicomPrinterConfig>

This configuration contains three main <Action> elements and a <Workflow> block containing one <If> condition. The actions are:

  • ParseContents: An instance of <ParseJobTextFile> , which retrieves the accession number and modality from the document filename.
  • Query: An instance of <Query>, which Queries and Retrieves additional attributes from a DICOM Worklist, and
  • Store: An instance of the <Store> action, which stores the resulting document and meta-data to PACS.

Making Changes

We will be adding:

  • An additional <Store> action for storage to a second PACS;
  • A <Print> action, for printing to film;
  • A <Run> action that will present the user with destination choices; and
  • A series of additional <If>conditions that enact the user’s selections.

Adding New Destinations

To add a new PACS destination we will need to drop a new <Store> action into the <ActionsList> block:

    <!-- First PACS -->
    <Store name="StoreToPrimary">
      <Compression type="RLE" />
      <ConnectionParameters>
        <PeerAeTitle>CONQUESTSRV1</PeerAeTitle>
        <Host>192.168.0.41</Host>
        <Port>5678</Port>
      </ConnectionParameters>
    </Store>

    <!-- Second PACS -->
    <Store name="StoreToSecondary">
      <Compression type="RLE" />
      <ConnectionParameters>
        <MyAeTitle>DICOM_PRINTER</MyAeTitle>
        <PeerAeTitle>CONQUESTSRV2</PeerAeTitle>
        <Host>192.168.0.42</Host>
        <Port>104</Port>
      </ConnectionParameters>
    </Store>
  </ActionsList>

Note that in order to help differentiate these elements in the config file, we’ve also renamed the first <Store> action from just “Store” to “StoreToPrimary”. We’ll have to update the <Perform>call in the <Workflow> block to reflect this change.

You may have also noticed that we added a <MyAeTitle> element. This tells DP2 to use a specific AE title when calling out to the second PACS.

Next, we add our new <Print> action:

...
  </Store>
  <Print name="Print">
    <PrintMode>Grayscale12</PrintMode>
    <Resolution>254 x 254</Resolution>
    <ConnectionParameters>
      <MyAeTitle>DICOM_PRINTER</MyAeTitle>
      <PeerAeTitle>DRIPIX</PeerAeTitle>
      <Host>192.168.0.43</Host>
      <Port>5040</Port>
    </ConnectionParameters>
  </Print>
</ActionsList>

This action references a printer located at 192.168.0.43, listening on port 5040, responding to the AE title DRYPIX. DryPix printers tend to use a resolution of 254/508 DPI. Specifying the resolution here helps with true-size image reproduction.

Saving Data

Next, we need to create a place to save our user responses. DP2 doesn’t have variables, but it does let us save things in private DICOM tags:

<ActionsList>
  <SetTag tag="(1001,1000)" tagName="Destination"
          name="InitDestination" vr="LO"/>
...

Using a private tag ensures that whatever we put there won’t interfere with actual DICOM meta-data.

GeneralSelectPlugin.exe

To get the user to choose a destination we’ll use DICOM Printer 2’s <Run> action, which allows you to bring up any external executable; in this case, GeneralSelectPlugin.exe, a tool included with DP2 for exactly this purpose.

<Run type="Interactive" name="ChooseDestination">
    <Command>plugins/GeneralSelectPlugin.exe</Command>
    <Timeout>60000</Timeout>
    <Arguments>Select Report Destination|Primary PACS|Secondary PACS|Dry Imager</Arguments>
    <Output tag="(1001,1000)"/>
    <!-- Destination -->
</Run>

We’ve given the user 60 seconds to select from among three options: “Primary PACS”, “Secondary PACS”, and “Dry Imager”. The selection will be stored to the (1001,1000)private attribute. If the user fails to make their selection within 60 seconds, then the run action will return in error, which can later be used to interrupt the workflow.


The Workflow

Next, we’ll need to assemble our Actions into the <Workflow> block.

Here is the order of events in our workflow:

  • Initialize our destination private tag;
  • Present the user with our destination choices, and store their response to our private tag;
  • Check if the user wishes to print to film, and do it.
  • If the user has chosen to send to either PACS, then query worklist, and then store to either one or both.

Choose Destination

<Perform action="InitDestination"/>
<Perform action="ChooseDestination"/ onError="Discard">

The line calls the InitDestination action, which initializes the (1001,1000)attribute with a blank value. The second calls the GeneralSelectPlugin.exe executable and stores the response. If the user closes the window without making a selection, or fails to make a choice within our timeout of 60 seconds, then the job will be “Discarded”.

Print to Film

Next, we check if the user wishes to print, and print if necessary.

<If field="TAG_VALUE( 1001, 1000 )" value="\*Dry\*">
  <Statements>
    <Perform action="Print" onError="Ignore"/>
  </Statements>
</If>

The <If> element checks if the value of our private tag contains the word “Dry”, which would indicate that this was one of the user selections. “\*Dry\*” is a wildcard match.

Parse, Query, and Store

If the user chose either of the destination PACS, then our private attribute will contain the word “PACS”.

<If field="TAG_VALUE( 1001, 1000 )" value="\*PACS\*">
  <Statements>
    ...
  </Statements>
</If>

If that’s the case, then we need to get a couple of attributes from the filename and Query the Worklist.

<Perform action="ParseContents" onError="Hold" />
<Perform action="Query" onError="Ignore" />

Then check if the query returned a result, and either proceed to storage, or retry the Query later.

<If field="QUERY_FOUND" value="1">
  <Statements>
    ...
  </Statements>
  <Else>
    <Suspend resumeAction="Query">
  </Else>
</If>

Finally, we check for the words “Primary” and “Secondary” in the user’s selection, and then store correspondingly:

<If field="TAG_VALUE( 1001, 1000 )" value="\*Primary\*">
  <Statements>
    <Perform action="StoreOnPrimaryPacs" onError="Ignore"/>
  </Statements>
</If>
<If field="TAG_VALUE( 1001, 1000 )" value="\*Secondary\*">
  <Statements>
    <Perform action="StoreOnSecondaryPacs" onError="Ignore"/>
  </Statements>
</If>

Plug-In Launcher

DICOM Printer 2 is a system service. Because if this it cannot communicate directly with the user space — or present user interface elements directly.

To help with this communication, DP2 comes accompanied by a tool called the “Plug-In Launcher”. In order for this all to function as designed you will have to make sure that Plug-In Launcher is started on user login. You can verify that it’s running by the presence of its icon in the task tray.

A shortcut to Plug-In Launcher can be found in your Start Menu.


The Full Configuration

Reproduced here for copy/paste purposes.

<DicomPrinterConfig>
  <General>
    <CheckingInterval>1</CheckingInterval>
    <SuspensionTime>1</SuspensionTime>
    <Verbosity>20</Verbosity>
  </General>
  <ActionsList>
    <SetTag tag="(1001,1000)" tagName="Destination"
          name="InitDestination" vr="LO"/>

    <Run type="Interactive" name="ChooseDestination">
        <Command>plugins/GeneralSelectPlugin.exe</Command>
        <Timeout>60000</Timeout>
        <Arguments>Select Report Destination|Primary PACS|Secondary PACS|Dry Imager</Arguments>
        <Output tag="(1001,1000)"/>
        <!-- Destination -->
    </Run>

    <ParseJobTextFile name="ParseContents">
      <DcmTag tag="(8,50)">^(.*)\-.*$</DcmTag>
      <DcmTag tag="(8,60)">^.*\-(.*)$</DcmTag>
    </ParseJobFileName>

    <Query name="Query" type="Worklist">
      <DcmTag tag="(0008,0050)" />
      <DcmTag tag="(0008,0060)" />
      <DcmTag tag="(0010,0020)" />
      <DcmTag tag="(0020,000D)" />
      <ConnectionParameters>
        <MyAeTitle>DICOMPRINTER2</MyAeTitle>
        <PeerAeTitle>DVT</PeerAeTitle>
        <Host>localhost</Host>
        <Port>104</Port>
      </ConnectionParameters>
    </Query>

    <!-- First PACS -->
    <Store name="StoreToPrimary">
      <Compression type="RLE" />
      <ConnectionParameters>
        <PeerAeTitle>CONQUESTSRV1</PeerAeTitle>
        <Host>192.168.0.41</Host>
        <Port>5678</Port>
      </ConnectionParameters>
    </Store>

    <!-- Second PACS -->
    <Store name="StoreToSecondary">
      <Compression type="RLE" />
      <ConnectionParameters>
        <MyAeTitle>DICOM_PRINTER</MyAeTitle>
        <PeerAeTitle>CONQUESTSRV2</PeerAeTitle>
        <Host>192.168.0.42</Host>
        <Port>104</Port>
      </ConnectionParameters>
    </Store>

    <Print name="Print">
      <PrintMode>Grayscale12</PrintMode>
      <Resolution>254 x 254</Resolution>
      <ConnectionParameters>
        <MyAeTitle>DICOM_PRINTER</MyAeTitle>
        <PeerAeTitle>DRIPIX</PeerAeTitle>
        <Host>192.168.0.43</Host>
        <Port>5040</Port>
      </ConnectionParameters>
    </Print>
  </ActionsList>
  </ActionsList>
  <Workflow>
    <Perform action="InitDestination"/>
    <Perform action="ChooseDestination"/ onError="Discard">
    
    <If field="TAG_VALUE( 1001, 1000 )" value="\*Dry\*">
      <Statements>
        <Perform action="Print" onError="Ignore"/>
      </Statements>
    </If>

    <If field="TAG_VALUE( 1001, 1000 )" value="\*PACS\*">
      <Statements>
        <Perform action="ParseContents" onError="Hold" />
        <Perform action="Query" onError="Ignore" />
        <If field="QUERY_FOUND" value="1">
          <Statements>
            <If field="TAG_VALUE( 1001, 1000 )" value="\*Primary\*">
              <Statements>
                <Perform action="StoreToPrimary" onError="Ignore"/>
              </Statements>
            </If>
            <If field="TAG_VALUE( 1001, 1000 )" value="\*Secondary\*">
              <Statements>
                <Perform action="StoreToSecondary" onError="Ignore"/>
              </Statements>
            </If>
          </Statements>
          <Else>
            <Suspend resumeAction="Query">
          </Else>
        </If>
      </Statements>
    </If>
    
  </Workflow>
</DicomPrinterConfig>

Public Test DICOM Modality Worklist

It’s so common that we need to test DMWL, that we thought it a good idea to just make one public.

Here are the details:

  • Host: worklist.fluxinc.ca
  • Port: 1070
  • AE Title: FLUX_WORKLIST
  • Your AE title: Anything you like

Note:

All records in this worklist are kept current, meaning, every exam is always today, eastern time. If you live east of the east coast and use the scheduled-date in your query, then you will have to query yesterday.

You can also check the status and current records at:
https://worklist.fluxinc.ca

By the way? Have you tried TuPACS, our Scan-to-PACS application?