DICOM Basics using Java - Understanding DICOM Structured Reports

Introduction

This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore DICOM Structured Reports (SR), which provide a standardized way to encode clinical findings, measurements, and observations. Unlike free-text reports, Structured Reports use a tree structure with coded concepts that enable computer processing and data mining.

Structured Reports are increasingly important in modern radiology workflows, especially with AI-assisted diagnosis, quantitative imaging, and clinical decision support systems.

Prerequisites

Before you begin, ensure you have the following:

  • Java JDK installed (Java 8 or later)
  • PixelMed Java DICOM Toolkit
  • Output directory (C:\temp) for saving generated files
  • You can find all the code demonstrated in this tutorial on GitHub here

“The greatest glory in living lies not in never falling, but in rising every time we fall.” ~ Nelson Mandela

The Theory Behind Structured Reporting

Structured Reports represent a fundamental shift from human-readable documentation to machine-processable clinical knowledge. This transformation enables what informatics researchers call "computable clinical content" - findings that can be automatically aggregated, analyzed, and integrated into decision support systems.

The Semantic Interoperability Problem

Traditional radiology reports are prose documents optimized for human readers. While effective for communication between physicians, they present significant challenges for computational use:

  • Ambiguity: "The lesion appears slightly larger" - larger than what? By how much?
  • Inconsistent Terminology: "mass," "nodule," "lesion," "growth" may mean the same thing
  • Implicit Context: "Normal" depends on patient age, clinical history, and comparison studies
  • Narrative Structure: Natural language processing struggles with complex medical prose

Structured Reports solve these problems by encoding clinical content as directed acyclic graphs (DAGs) of coded concepts with explicit relationships and quantitative values.

The SR Content Tree Model

An SR document is fundamentally a tree structure where:

  • Nodes are Content Items (containers, codes, text, numbers, images, etc.)
  • Edges are Relationships (CONTAINS, HAS OBS CONTEXT, INFERRED FROM, etc.)
  • Leaves are terminal values (measurements, coded findings, text descriptions)

This tree structure maps naturally to clinical reasoning: a finding CONTAINS sub-findings, which are INFERRED FROM specific images, observed by a particular physician (HAS OBS CONTEXT).

The Role of Coding Systems

Structured Reports derive their power from standardized coding systems that provide unambiguous concept identification:

  • SNOMED CT: ~350,000 clinical concepts covering anatomy, findings, procedures
  • LOINC: Laboratory and clinical observations, document types
  • RadLex: Radiology-specific lexicon with ~75,000 terms
  • UCUM: Unified Code for Units of Measure (ensuring "5 cm" is unambiguous)

When a report encodes a finding as RadLex code "RID4919" rather than the text "hepatocellular carcinoma," it becomes globally unambiguous, translatable, and computationally processable.

AI/ML Integration

Structured Reports are increasingly important as the output format for AI/ML algorithms in radiology. When a deep learning model detects a pulmonary nodule, encoding its findings as a TID 1500 Measurement Report ensures that:

  • The finding can be displayed by any SR-capable viewer
  • Measurements can be automatically tracked longitudinally
  • Decision support systems can process the AI output
  • The finding links back to the specific image and coordinates

Understanding Structured Reports

DICOM Structured Reports differ from traditional text reports in several important ways:

AspectText ReportStructured Report
FormatFree textHierarchical tree
EncodingPlain textCoded concepts
Machine readableLimitedFull support
StandardsVariesDICOM, SNOMED, LOINC
Data miningDifficultStraightforward

SR Template Types

DICOM defines several SR SOP Classes for different use cases:

SOP ClassUIDUse Case
Basic Text SR1.2.840.10008.5.1.4.1.1.88.11Simple text reports
Enhanced SR1.2.840.10008.5.1.4.1.1.88.22Numeric measurements
Comprehensive SR1.2.840.10008.5.1.4.1.1.88.33Full functionality
Comprehensive 3D SR1.2.840.10008.5.1.4.1.1.88.34With 3D coordinates

SR Content Structure

Structured Reports use Content Items as building blocks. Each item contains:

  • Value Type: CONTAINER, TEXT, CODE, NUM, DATE, TIME, PNAME, UIDREF, IMAGE
  • Concept Name: Coded entry describing what the item represents
  • Value: The actual content (text, code, number, etc.)
  • Relationship: How this item relates to its parent (CONTAINS, HAS OBS CONTEXT, etc.)

Step 1: Setting Up the Demo

Here's how to create a Structured Report using PixelMed:

package com.saravanansubramanian.dicom.pixelmedtutorial;

import java.text.SimpleDateFormat;
import java.util.Date;

import com.pixelmed.dicom.*;

public class CreateStructuredReportDemo {

    public static void main(String[] args) {

        try {

            System.out.println("=== DICOM Structured Report Creation Demo ===\n");

            // Output file path
            String outputFilePath = "C:\\temp\\sample_sr.dcm";

            // Create the SR
            AttributeList srList = createChestXRayReport();

            // Add file meta information
            FileMetaInformation.addFileMetaInformation(
                srList,
                TransferSyntax.ExplicitVRLittleEndian,
                "OurSourceAET"
            );

            // Write to file
            srList.write(outputFilePath);

            System.out.println("Structured Report created: " + outputFilePath);
            System.out.println("\n--- SR Contents ---");
            System.out.println(srList.toString());

        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
    }
}

Step 2: Building Patient and Study Information

Like any DICOM object, Structured Reports require patient and study identification:

private static AttributeList createChestXRayReport() throws Exception {

    AttributeList list = new AttributeList();

    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
    SimpleDateFormat timeFormat = new SimpleDateFormat("HHmmss");
    Date now = new Date();
    String currentDate = dateFormat.format(now);
    String currentTime = timeFormat.format(now);

    // === SOP Common Module ===
    Attribute sopClassUID = new UniqueIdentifierAttribute(TagFromName.SOPClassUID);
    sopClassUID.addValue(SOPClass.BasicTextSRStorage);
    list.put(sopClassUID);

    Attribute sopInstanceUID = new UniqueIdentifierAttribute(TagFromName.SOPInstanceUID);
    sopInstanceUID.addValue(UniqueIdentifierAttribute.createUID());
    list.put(sopInstanceUID);

    // === Patient Module ===
    Attribute patientName = new PersonNameAttribute(TagFromName.PatientName);
    patientName.addValue("Doe^John^A");
    list.put(patientName);

    Attribute patientID = new LongStringAttribute(TagFromName.PatientID);
    patientID.addValue("PAT123456");
    list.put(patientID);

    Attribute patientBirthDate = new DateAttribute(TagFromName.PatientBirthDate);
    patientBirthDate.addValue("19650315");
    list.put(patientBirthDate);

    Attribute patientSex = new CodeStringAttribute(TagFromName.PatientSex);
    patientSex.addValue("M");
    list.put(patientSex);

    // === General Study Module ===
    Attribute studyInstanceUID = new UniqueIdentifierAttribute(TagFromName.StudyInstanceUID);
    studyInstanceUID.addValue(UniqueIdentifierAttribute.createUID());
    list.put(studyInstanceUID);

    Attribute studyDate = new DateAttribute(TagFromName.StudyDate);
    studyDate.addValue(currentDate);
    list.put(studyDate);

    Attribute studyTime = new TimeAttribute(TagFromName.StudyTime);
    studyTime.addValue(currentTime);
    list.put(studyTime);

    Attribute accessionNumber = new ShortStringAttribute(TagFromName.AccessionNumber);
    accessionNumber.addValue("ACC123456");
    list.put(accessionNumber);

    Attribute referringPhysician = new PersonNameAttribute(TagFromName.ReferringPhysicianName);
    referringPhysician.addValue("Smith^Jane^Dr");
    list.put(referringPhysician);

    Attribute studyID = new ShortStringAttribute(TagFromName.StudyID);
    studyID.addValue("STUDY001");
    list.put(studyID);

    // Continue with series and document modules...
}

Step 3: Adding SR Document Modules

Structured Reports have specific modules for series and document information:

// === SR Document Series Module ===
Attribute modality = new CodeStringAttribute(TagFromName.Modality);
modality.addValue("SR");
list.put(modality);

Attribute seriesInstanceUID = new UniqueIdentifierAttribute(TagFromName.SeriesInstanceUID);
seriesInstanceUID.addValue(UniqueIdentifierAttribute.createUID());
list.put(seriesInstanceUID);

Attribute seriesNumber = new IntegerStringAttribute(TagFromName.SeriesNumber);
seriesNumber.addValue("1");
list.put(seriesNumber);

// === SR Document General Module ===
Attribute instanceNumber = new IntegerStringAttribute(TagFromName.InstanceNumber);
instanceNumber.addValue("1");
list.put(instanceNumber);

Attribute contentDate = new DateAttribute(TagFromName.ContentDate);
contentDate.addValue(currentDate);
list.put(contentDate);

Attribute contentTime = new TimeAttribute(TagFromName.ContentTime);
contentTime.addValue(currentTime);
list.put(contentTime);

// Completion flag - COMPLETE or PARTIAL
Attribute completionFlag = new CodeStringAttribute(TagFromName.CompletionFlag);
completionFlag.addValue("COMPLETE");
list.put(completionFlag);

// Verification flag - VERIFIED or UNVERIFIED
Attribute verificationFlag = new CodeStringAttribute(TagFromName.VerificationFlag);
verificationFlag.addValue("VERIFIED");
list.put(verificationFlag);

Step 4: Building the Content Tree

The heart of a Structured Report is its content tree. PixelMed's ContentItemFactory helps build this:

// === Build the SR Content Tree ===
ContentItemFactory cif = new ContentItemFactory();

// Root container - document title
ContentItem root = cif.new ContainerContentItem(
    null, // parent (null for root)
    null, // relationship type (null for root)
    new CodedSequenceItem("18782-3", "LN", "Radiology Study Observation"),
    true, // continuity of content: SEPARATE
    "DCMR", // template mapping resource
    "2000" // template identifier
);

// Add observation context - observer type
ContentItem observerContext = cif.new CodeContentItem(
    root,
    "HAS OBS CONTEXT",
    new CodedSequenceItem("121005", "DCM", "Observer Type"),
    new CodedSequenceItem("121006", "DCM", "Person")
);

// Add observer name
ContentItem observerName = cif.new PersonNameContentItem(
    root,
    "HAS OBS CONTEXT",
    new CodedSequenceItem("121008", "DCM", "Person Observer Name"),
    "Johnson^Robert^Dr"
);

Step 5: Adding Findings and Impressions

Clinical findings are organized in sections within the content tree:

// Add findings section
ContentItem findingsSection = cif.new ContainerContentItem(
    root,
    "CONTAINS",
    new CodedSequenceItem("121070", "DCM", "Findings"),
    true,
    null,
    null
);

// Add text findings
ContentItem finding1 = cif.new TextContentItem(
    findingsSection,
    "CONTAINS",
    new CodedSequenceItem("121071", "DCM", "Finding"),
    "The heart size is within normal limits. No cardiomegaly."
);

ContentItem finding2 = cif.new TextContentItem(
    findingsSection,
    "CONTAINS",
    new CodedSequenceItem("121071", "DCM", "Finding"),
    "The lungs are clear without focal consolidation, effusion, or pneumothorax."
);

ContentItem finding3 = cif.new TextContentItem(
    findingsSection,
    "CONTAINS",
    new CodedSequenceItem("121071", "DCM", "Finding"),
    "The mediastinal contours are unremarkable."
);

// Add impression section
ContentItem impressionSection = cif.new ContainerContentItem(
    root,
    "CONTAINS",
    new CodedSequenceItem("121072", "DCM", "Impression"),
    true,
    null,
    null
);

ContentItem impression = cif.new TextContentItem(
    impressionSection,
    "CONTAINS",
    new CodedSequenceItem("121073", "DCM", "Impression Description"),
    "No acute cardiopulmonary abnormality."
);

// Convert the content tree to DICOM attributes
StructuredReport sr = new StructuredReport(root);
AttributeList srContentList = sr.getAttributeList();

// Copy SR content to our list
list.putAll(srContentList);

return list;

Coded Vocabularies

Structured Reports use standard coding schemes for interoperability:

SchemeDesignatorExamples
DICOMDCMDocument types, relationships
LOINCLNLab tests, document sections
SNOMED CTSCTClinical concepts, anatomy
RadLexRADLEXRadiology-specific terms
UCUMUCUMUnits of measurement

Relationship Types

Content items are linked using defined relationship types:

RelationshipDescription
CONTAINSParent contains child items
HAS OBS CONTEXTObservation context (who, when)
HAS ACQ CONTEXTAcquisition context
HAS CONCEPT MODConcept modifier
INFERRED FROMDerived from other content
SELECTED FROMReference to source data

Important Considerations

  • Template Compliance: Follow DICOM SR templates for specific use cases (TID 1500 for measurements, etc.)
  • Code Selection: Use appropriate coding schemes for your domain
  • Verification: Set appropriate completion and verification flags
  • References: Link to source images using IMAGE content items when applicable

Conclusion

DICOM Structured Reports provide a powerful way to encode clinical findings in a machine-readable format. While more complex than plain text reports, they enable advanced workflows like automated data extraction, clinical decision support, and integration with AI systems.

Understanding the tree structure, coded concepts, and relationship types is key to creating compliant Structured Reports that can be exchanged and processed across different healthcare systems. In the next tutorial in this series, I will cover DICOM transfer syntaxes and image compression. See you then!