DICOM identifier configuration and customization

Introduction

When XNAT receives a DICOM object, it uses a set of identifier objects to determine to which project the newly received DICOM object belongs, and to assign subject and session labels. These identifiers are applied to objects received by the C-STORE SCP (receiver), but also to objects received from more obscure paths including RSNA MIRC-style file-by-file HTTP(S) transport and zip file upload to the importer REST service. The identifier objects see the DICOM metadata before any DicomEdit scripts are applied – necessarily, since XNAT needs to identify the project before it can choose the correct DicomEdit script.

XNAT's default identifiers can be replaced to fully customize handling of received DICOM objects. Situations that might call for customized identifiers include:

  • Scanner operators put session identifying information into the DICOM metadata in a way that is consistently applied, but incompatible with the default XNAT identification rules
  • Identifying information is held in an external database, which can be queried using a key in the DICOM metadata, e.g., the study date and time or the study instance UID
  • Operators put session identifying information into the DICOM metadata inconsistently, and objects not meeting some criteria should be shunted into a quarantine project prearchive

The default C-STORE SCP configuration

The XNAT C-STORE SCP is configured in a Spring configuration file, $XNAT_HOME/projects/project/src/conf/dicom-import-context.xml . The default configuration creates an SCP on port 8104, with one AE titled XNAT, which uses the XNAT 1.5 rules for deriving project, subject, and session identities.

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

<bean name="dicomObjectIdentifier" class="org.nrg.dcm.id.ClassicDicomObjectIdentifier">
	<property name="userProvider" ref="receivedFileUserProvider" />
</bean>

<bean name="dicomSCPExecutor" class="java.util.concurrent.Executors"
factory-method="newCachedThreadPool" />

<bean name="dicomSCP" class="org.nrg.dcm.DicomSCP" factory-method="create">
	<constructor-arg ref="dicomSCPExecutor" />
	<constructor-arg value="8104" />
	<constructor-arg value="XNAT" />
	<constructor-arg ref="receivedFileUserProvider" />
	<constructor-arg ref="dicomObjectIdentifier" />
</bean>

</beans>
This configuration file is easiest to understand reading bottom to top. The last object ("bean") created is the DICOM SCP. This file uses the simplest form of the static factory method DicomSCP.create( ... ) : the arguments are a thread pool, defined earlier in the file as dicomSCPExecutor, to handle incoming requests; the TCP port number; the AE title; an object that supplies the XNAT user identity for the SCP ( receivedFileUserProvider, defined in the main XNAT Spring configuration file root-spring-config.xml ); and an object dicomObjectIdentifier that implements the rules for extracting project, subject, and session identities for each received file.
 
The next-to-last object is a thread pool for handing requests; we use here a simple pool that creates as many threads as needed. The Executors class contains several alternative static factory methods if different thread management is desired.
The first object created in this configuration file is the policy object for identity extraction; the ClassicDicomObjectIdentifier class uses the XNAT 1.5 rules.

Custom project identification

The rules for determining the project, subject, and session identity of received DICOM objects are fully customizable. The Java interface org.nrg.xnat.DicomObjectIdentifer<ProjectT>, defined in dicom-xnat-util, specifies the behavior required to customize identity extraction:

public interface DicomObjectIdentifier<ProjectT> {
 /** 
   * Determines to which project a specified DICOM object belongs 
   * @param o DicomObject 
   * @return project object 
   */
 ProjectT getProject(DicomObject o);
 
 String getSessionLabel(DicomObject o);
 String getSubjectLabel(DicomObject o);
 
 /** 
   * What DICOM attributes does this identifier use? 
   * @return sorted set of DICOM attribute tags 
   */
 SortedSet<Integer> getTags();
 
 /** 
   * Does this object request autoarchiving? 
   * @param o DicomObject 
   * @return true if object requests autoarchiving 
   * @return false if object requests no autoarchiving 
   * @return null if object does not specify autoarchiving 
   */
 Boolean requestsAutoarchive(DicomObject o);
}
 
To build an identifier for DicomSCP, the generic type ProjectT must be org.nrg.xnat.om.XnatProjectdata, XNAT's internal representation of a project. This requirement is easily met by instantiating or extending the class org.nrg.dcm.id.CompositeDicomObjectIdentifier, defined in$XNAT_HOMEplugin-resources/webapp/xnat/java . CompositeDicomObjectIdentifer builds a DicomObjectIdentifer` (the piece that DicomSCP uses) from parts that individually extract the project object and the subject and session names.

Example: Single-project AE

As a concrete example, let's add an AE that adds all received files to a project named Example1. The title of this project-specific AE will also be Example1.
First, we need a component that always returns the project name Example1. We write a trivial implementation for the interface org.nrg.dcm.id.DicomProjectIdentifer :

public final class FixedDicomProjectIdentifer implements DicomProjectIdentifier {
	private final String name;
 
 	public FixedDicomProjectIdentifier(final String name) {
		this.name = name;
 	}
 
	public SortedSet<Integer> getTags() {
		return new TreeSet<Integer>();
	}
 
	public XnatProjectdata apply(XDATUser user, DicomObject o) {
		return XnatProjectdata.getProjectByIDorAlias(name, user, false);
	}
}

(Some details such as import statements have been omitted in this simplified adaptation of org.nrg.dcm.id.FixedDicomProjectIdentifier, defined in $XNAT_HOME/plugin-resources/webapp/xnat .)

Now we can modify the configuration to use this custom project identifier, together with the standard subject and session identification rules in a CompositeDicomObjectIdentifier :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

<bean name="dicomObjectIdentifier" class="org.nrg.dcm.id.ClassicDicomObjectIdentifier">
	<property name="userProvider" ref="receivedFileUserProvider" />
</bean>
 
<bean name="dicomSCPExecutor" class="java.util.concurrent.Executors"
factory-method="newCachedThreadPool" />
 
<bean name="dcmtProjectIdent" class="org.nrg.dcm.id.FixedDicomProjectIdentifier">
	<constructor-arg value="dcmt"/>
</bean>
 
<bean name="baseSubjectIdent" class="org.nrg.dcm.id.ClassicDicomObjectIdentifier" factory-method="getSessionExtractors"/>
<bean name="baseSessionIdent" class="org.nrg.dcm.id.ClassicDicomObjectIdentifier" factory-method="getSubjectExtractors"/>
<bean name="baseAAIdent" class="org.nrg.dcm.id.ClassicDicomObjectIdentifier" factory-method="getAAExtractors"/>
 
<bean name="dcmtObjectIdent" class="org.nrg.dcm.id.CompositeDicomObjectIdentifier">
	<constructor-arg ref="dcmtProjectIdent"/>
	<constructor-arg ref="baseSubjectIdent"/>
	<constructor-arg ref="baseSessionIdent"/>
	<constructor-arg ref="baseAAIdent"/>
	<property name="userProvider" ref="receivedFileUserProvider"/>
</bean>
 
<util:map id="cstores">
	<entry key="dcmt" value-ref="dcmtObjectIdent"/>
	<entry key="XNAT" value-ref="dicomObjectIdentifier"/>
</util:map>
 
<bean name="dicomSCP" class="org.nrg.dcm.DicomSCP" factory-method="create">
	<constructor-arg ref="dicomSCPExecutor" />
	<constructor-arg value="8104" />
	<constructor-arg ref="receivedFileUserProvider" />
	<constructor-arg ref="cstores"/>
</bean>
</beans>

The DicomSCP object is once again defined at the bottom of the file, but instead of AE title and identifier arguments, we provide a map from AE title to identifier. In that map ( cstores ), we define two AEs. dcmt uses the custom DICOM object identifer dcmtObjectIdent, a CompositeDicomObjectIdentifer using the custom project identifier dcmtProjectIdent (an instance of our custom class FixedDicomProjectIdentifier ) and XNAT default identifiers for subject and session (obtained from static factory methods in ClassicDicomObjectIdentifier ). One additional identifier is necessary in this case: baseAAIdent determines whether the user has requested specific autoarchiving behavior for this session, overriding the project settings.
The AE XNAT uses the default identifiers defined by ClassicDicomObjectIdentifier for all extracted fields.

Custom file naming

Each DICOM object is saved to disk by the C-STORE receiver, with a name derived from the object's metadata. The naming is by default performed by the class org.nrg.dcm.xnat.SOPHashDicomFileNamer (defined in the dicom-xnat package), but different naming policies can be adopted by writing custom implementations of the DicomFileNamer interface:

/** * Copyright (c) 2010 Washington University */
package org.nrg.dcm;
import org.dcm4che2.data.DicomObject;
 
/** * @author Kevin A. Archie <karchie@wustl.edu> * Makes up a filename for a DICOM object. */
 
public interface DicomFileNamer {
 /** * Make up a filename for a DICOM object. * @param o DicomObject to be saved to a file. * @return filename to which the object should be saved */
 String makeFileName(DicomObject o);
}

For example, this class defines a naming policy similar to the default policy but with the instance number zero-padded:

/** * Copyright (c) 2010,2012 Washington University */
package org.nrg.dcm.xnat;
import java.nio.file.Files;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * * FileNamer that uses an alphanumeric hash of the SOP Class and SOP Instance
 * UIDs * to construct filenames, and a provided format string to transform the
 * instance number * component. Any spaces in the formatted instance number are
 * replaced by '0'. * @author Kevin A. Archie <karchie@wustl.edu>
 */
public final class FormattedInstanceSOPHashDicomFileNamer implements
		DicomFileNamer {
	private static final String FIELD_SEPARATOR = ".";
	private static final String SUFFIX = ".dcm";
	private final Logger logger = LoggerFactory
			.getLogger(FormattedInstanceSOPHashDicomFileNamer.class);
	private final String instanceNumberFormat;
	public FormattedInstanceSOPHashDicomFileNamer(final String format) {
		this.instanceNumberFormat = format;
	}
	/*
	 * (non-Javadoc) * @see
	 * org.nrg.dcm.DicomFileNamer#makeFileName(org.dcm4che2.data.DicomObject)
	 */
	public String makeFileName(final DicomObject o) {
		final List<String> components = Lists.newArrayList();
		final String studyID = o.getString(Tag.StudyID, "NULL");
		if (logger.isTraceEnabled()) {
			logger.trace(
					"Study {} @{} {} - {}:{}",
					new String[] { o.getString(Tag.StudyDescription),
							o.getString(Tag.StudyDate),
							o.getString(Tag.StudyTime),
							o.getString(Tag.SeriesNumber),
							o.getString(Tag.InstanceNumber) });
		}
		addIfNotNull(components, o.getString(Tag.PatientName, studyID),
				Labels.toLabelChars(o.getString(Tag.Modality)),
				Labels.toLabelChars(o.getString(Tag.StudyDescription)),
				Labels.toLabelChars(o.getString(Tag.SeriesNumber)));
		final String instanceNumber = Labels.toLabelChars(o
				.getString(Tag.InstanceNumber));
		if (!Strings.isNullOrEmpty(instanceNumber)) {
			components.add(String.format(instanceNumberFormat, instanceNumber)
					.replace(' ', '0'));
		}
		final String studyDate = o.getString(Tag.StudyDate);
		if (null != studyDate) {
			components.add(studyDate.replace(".", ""));
		}
		final String studyTime = o.getString(Tag.StudyTime);
		if (null != studyTime) {
			String fixedTime = studyTime.replace(":", ""); // fix ACR-NEMA
															// standard
															// 300-style times
			final int msecStart = studyTime.indexOf(".");
			if (msecStart >= 6) {
				fixedTime = fixedTime.substring(0, msecStart);
			}
			components.add(fixedTime);
		}
		final String sopClassUID = o.getString(Tag.SOPClassUID);
		final String instanceUID = o.getString(Tag.SOPInstanceUID);
		int hash = (null == sopClassUID) ? 0 : sopClassUID.hashCode();
		if (null != instanceUID) {
			hash = 37 * instanceUID.hashCode() + hash;
		}
		components.add(Long.toString(hash & 0xffffffffl, 36));
		final Joiner joiner = Joiner.on(FIELD_SEPARATOR);
		final StringBuilder fileName = joiner.appendTo(new StringBuilder(),
				components);
		fileName.append(SUFFIX);
		return Files.toFileNameChars(fileName.toString());
	}
	private <T> Collection<T> addIfNotNull(final Collection<T> coll,
			final T... ts) {
		for (final T t : ts) {
			if (null != t) {
				coll.add(t);
			}
		}
		return coll;
	}
}

A custom namer can be inserted via the SCP configuration file:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
  
  <bean name="dicomObjectIdentifier" class="org.nrg.dcm.id.ClassicDicomObjectIdentifier">
    <property name="userProvider" ref="receivedFileUserProvider"/>
  </bean>
  
  <bean name="dicomFileNamer" class="org.nrg.dcm.xnat.FormattedInstanceSOPHashDicomFileNamer">
    <constructor-arg value="%5s"/>
  </bean>
  
  <bean name="dicomSCPExecutor" class="java.util.concurrent.Executors" factory-method="newCachedThreadPool"/>
  
  <bean name="dicomSCP" class="org.nrg.dcm.DicomSCP" factory-method="create">
    <constructor-arg ref="dicomSCPExecutor"/>
    <constructor-arg value="8104"/>
    <constructor-arg ref="receivedFileUserProvider"/>
    <constructor-arg value="XNAT"/>
    <constructor-arg ref="dicomObjectIdentifier"/>
    <constructor-arg ref="dicomFileNamer"/>
  </bean>
  
  <bean name="dicomSCPManager" class="org.nrg.dcm.DicomSCPManager">
    <constructor-arg ref="dicomSCP"/>
  </bean>


</beans>


$label.name