DICOM Basics using .NET and C# - 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 in a structured, machine-readable format.
Unlike traditional text reports, Structured Reports encode information using coded concepts and a hierarchical tree structure. This enables automated processing, data mining, and integration with clinical decision support systems.
Prerequisites
Before you begin, ensure you have the following:
- A .NET development environment (Visual Studio or Visual Studio Code)
- The Fellow Oak DICOM library (fo-dicom) installed via NuGet
- Basic understanding of DICOM concepts from previous tutorials
- You can find all the code demonstrated in this tutorial on GitHub here
“Time is the substance from which I am made. Time is a river which carries me along, but I am the river.” ~ Jorge Luis Borges
The Theory Behind Structured Reports
Traditional radiology reports are narrative text: prose written by radiologists describing their findings. While human-readable, narrative reports are semantically opaque to computers. You cannot easily extract that a lesion measures 2.3cm or that a finding is in the right upper lobe without natural language processing. Structured Reports solve this by encoding clinical content as discrete, machine-readable data elements.
The SR content tree model represents clinical observations as a hierarchy of typed nodes. Each node has a relationship to its parent (CONTAINS, HAS PROPERTIES, etc.), a value type (TEXT, CODE, NUM, IMAGE), and a concept name indicating what the node represents. This creates a semantic graph where meaning is explicit rather than implied. A finding doesn't just contain the text "2.3cm" - it explicitly encodes a numeric measurement with units, linked to the anatomical location it measures.
The use of coded concepts from terminologies like SNOMED CT, LOINC, and RadLex enables semantic interoperability. When a radiologist reports "hepatocellular carcinoma," the code 25370001 from SNOMED CT conveys the same meaning regardless of language, local terminology variations, or abbreviations. This codification enables decision support systems to recognize findings, aggregate data for research, and support quality metrics.
The IMAGE reference type creates direct links between observations and evidence. When a measurement refers to a specific image, the SR can encode the exact SOP Instance UID and spatial coordinates. This traceability means you can navigate from a finding directly to the image that supports it, essential for verification and follow-up comparison.
SR templates (defined in DICOM Part 16) provide document patterns for specific use cases. A CAD SR for mammography follows a different structure than a dose report. Templates constrain what content items are required, optional, or prohibited, ensuring that SR documents from different systems are structurally comparable. This is the foundation for meaningful data exchange and analytics.
Understanding Structured Reports
DICOM Structured Reports encode clinical content as a tree of content items. Each content item has:
- Value Type: The type of content (CODE, TEXT, NUM, IMAGE, etc.)
- Relationship Type: How this item relates to its parent (CONTAINS, HAS PROPERTIES, etc.)
- Concept Name: What this item represents (coded concept)
- Value: The actual content based on the value type
SR Content Types
Structured Reports support various content types:
| Value Type | Contains | Example |
|---|---|---|
| TEXT | Free-text description | ”No acute findings” |
| CODE | Coded concept | Diagnostic code from SNOMED |
| NUM | Numeric measurement with units | ”5.2 cm” |
| IMAGE | Reference to DICOM image | Image SOP Instance UID |
| UIDREF | Reference by UID | Related SR or other object |
| CONTAINER | Group of related items | ”Findings” section |
Common SR SOP Classes
| SOP Class | UID | Use Case |
|---|---|---|
| Basic Text SR | 1.2.840.10008.5.1.4.1.1.88.11 | Simple text reports |
| Enhanced SR | 1.2.840.10008.5.1.4.1.1.88.22 | More structure and codes |
| Comprehensive SR | 1.2.840.10008.5.1.4.1.1.88.33 | Full SR capabilities |
| X-Ray Radiation Dose SR | 1.2.840.10008.5.1.4.1.1.88.67 | Dose reporting |
| Mammography CAD SR | 1.2.840.10008.5.1.4.1.1.88.50 | CAD results |
Step 1 of 3: Creating a Simple Structured Report
Let's create a Basic Text Structured Report:
using System;
using System.Diagnostics;
using System.IO;
using FellowOakDicom;
namespace StructuredReportsExample
{
public class Program
{
private static readonly string OutputPath =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Output");
private static readonly string OutputSrFile =
Path.Combine(OutputPath, "sample_structured_report.dcm");
public static void Main(string[] args)
{
try
{
LogToDebugConsole("=== DICOM Structured Reports Tutorial ===");
// Ensure output directory exists
if (!Directory.Exists(OutputPath))
{
Directory.CreateDirectory(OutputPath);
}
// Create and save a sample SR
CreateSampleStructuredReport();
// Read and display the created SR
ReadStructuredReport(OutputSrFile);
LogToDebugConsole("Structured Report tutorial completed.");
}
catch (Exception e)
{
LogToDebugConsole($"Error: {e.Message}");
}
}
private static void LogToDebugConsole(string message)
{
Debug.WriteLine(message);
}
}
}
Step 2 of 3: Building the SR Dataset
Now let's build the complete Structured Report with all required modules:
private static void CreateSampleStructuredReport()
{
LogToDebugConsole("--- Creating Sample Structured Report ---");
var dataset = new DicomDataset();
// Patient Module
dataset.Add(DicomTag.PatientName, "Doe^John");
dataset.Add(DicomTag.PatientID, "SR123456");
dataset.Add(DicomTag.PatientBirthDate, "19800101");
dataset.Add(DicomTag.PatientSex, "M");
// Study Module
var studyUid = DicomUID.Generate();
dataset.Add(DicomTag.StudyInstanceUID, studyUid);
dataset.Add(DicomTag.StudyDate, DateTime.Now.ToString("yyyyMMdd"));
dataset.Add(DicomTag.StudyTime, DateTime.Now.ToString("HHmmss"));
dataset.Add(DicomTag.AccessionNumber, "SR-ACC-001");
dataset.Add(DicomTag.StudyDescription, "Chest CT Findings Report");
// Series Module
var seriesUid = DicomUID.Generate();
dataset.Add(DicomTag.SeriesInstanceUID, seriesUid);
dataset.Add(DicomTag.SeriesNumber, "1");
dataset.Add(DicomTag.Modality, "SR"); // Structured Report modality
dataset.Add(DicomTag.SeriesDescription, "CT Findings SR");
// SOP Common Module - Basic Text SR
var sopUid = DicomUID.Generate();
dataset.Add(DicomTag.SOPClassUID, DicomUID.BasicTextSRStorage);
dataset.Add(DicomTag.SOPInstanceUID, sopUid);
dataset.Add(DicomTag.InstanceNumber, "1");
// SR Document General Module
dataset.Add(DicomTag.InstanceCreationDate, DateTime.Now.ToString("yyyyMMdd"));
dataset.Add(DicomTag.InstanceCreationTime, DateTime.Now.ToString("HHmmss"));
dataset.Add(DicomTag.ContentDate, DateTime.Now.ToString("yyyyMMdd"));
dataset.Add(DicomTag.ContentTime, DateTime.Now.ToString("HHmmss"));
// Completion Flag: COMPLETE or PARTIAL
dataset.Add(DicomTag.CompletionFlag, "COMPLETE");
// Verification Flag: VERIFIED or UNVERIFIED
dataset.Add(DicomTag.VerificationFlag, "UNVERIFIED");
// SR Document Content Module - Content Sequence
var contentSequence = new DicomSequence(DicomTag.ContentSequence);
// Add a TEXT content item with findings
var findingsItem = new DicomDataset();
findingsItem.Add(DicomTag.RelationshipType, "CONTAINS");
findingsItem.Add(DicomTag.ValueType, "TEXT");
// Concept Name Code Sequence - What this item represents
var conceptNameSeq = new DicomSequence(DicomTag.ConceptNameCodeSequence);
var conceptName = new DicomDataset();
conceptName.Add(DicomTag.CodeValue, "121071");
conceptName.Add(DicomTag.CodingSchemeDesignator, "DCM");
conceptName.Add(DicomTag.CodeMeaning, "Finding");
conceptNameSeq.Items.Add(conceptName);
findingsItem.Add(conceptNameSeq);
// The actual text value
findingsItem.Add(DicomTag.TextValue,
"No acute cardiopulmonary abnormality. " +
"Clear lung fields bilaterally. Heart size within normal limits.");
contentSequence.Items.Add(findingsItem);
dataset.Add(contentSequence);
// Save the Structured Report
var dicomFile = new DicomFile(dataset);
dicomFile.Save(OutputSrFile);
LogToDebugConsole($"Created SR file: {OutputSrFile}");
}
Step 3 of 3: Reading Structured Reports
Now let's read and navigate the SR content tree:
private static void ReadStructuredReport(string filePath)
{
LogToDebugConsole("--- Reading Structured Report ---");
var file = DicomFile.Open(filePath);
var dataset = file.Dataset;
LogToDebugConsole($" SOP Class: {dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, "")}");
LogToDebugConsole($" Modality: {dataset.GetSingleValueOrDefault(DicomTag.Modality, "")}");
LogToDebugConsole($" Patient: {dataset.GetSingleValueOrDefault(DicomTag.PatientName, "")}");
LogToDebugConsole($" Study: {dataset.GetSingleValueOrDefault(DicomTag.StudyDescription, "")}");
LogToDebugConsole($" Status: {dataset.GetSingleValueOrDefault(DicomTag.CompletionFlag, "")}");
// Navigate Content Sequence
var contentSequence = dataset.GetSequence(DicomTag.ContentSequence);
if (contentSequence != null)
{
LogToDebugConsole(" Content Items:");
foreach (var item in contentSequence.Items)
{
var valueType = item.GetSingleValueOrDefault(DicomTag.ValueType, "");
var relationshipType = item.GetSingleValueOrDefault(DicomTag.RelationshipType, "");
LogToDebugConsole($" - Type: {valueType}, Relationship: {relationshipType}");
// Get concept name if present
var conceptNameSeq = item.GetSequence(DicomTag.ConceptNameCodeSequence);
if (conceptNameSeq != null && conceptNameSeq.Items.Count > 0)
{
var codeMeaning = conceptNameSeq.Items[0].GetSingleValueOrDefault(
DicomTag.CodeMeaning, "");
LogToDebugConsole($" Concept: {codeMeaning}");
}
// Get value based on type
if (valueType == "TEXT")
{
var textValue = item.GetSingleValueOrDefault(DicomTag.TextValue, "");
LogToDebugConsole($" Text: {textValue}");
}
else if (valueType == "NUM")
{
var numValue = item.GetSingleValueOrDefault(DicomTag.NumericValue, "");
LogToDebugConsole($" Numeric Value: {numValue}");
}
else if (valueType == "CODE")
{
var codeSeq = item.GetSequence(DicomTag.ConceptCodeSequence);
if (codeSeq != null && codeSeq.Items.Count > 0)
{
var code = codeSeq.Items[0].GetSingleValueOrDefault(DicomTag.CodeMeaning, "");
LogToDebugConsole($" Code: {code}");
}
}
}
}
}
Sample output:
=== DICOM Structured Reports Tutorial ===
--- Creating Sample Structured Report ---
Created SR file: C:\...\Output\sample_structured_report.dcm
--- Reading Structured Report ---
SOP Class: 1.2.840.10008.5.1.4.1.1.88.11
Modality: SR
Patient: Doe^John
Study: Chest CT Findings Report
Status: COMPLETE
Content Items:
- Type: TEXT, Relationship: CONTAINS
Concept: Finding
Text: No acute cardiopulmonary abnormality. Clear lung fields bilaterally. Heart size within normal limits.
Structured Report tutorial completed.
Common Use Cases for Structured Reports
- Radiation Dose Reporting: Automatic capture of CT dose information
- CAD Results: Computer-aided detection findings for mammography
- Key Image Notes: Annotations and significant findings
- Measurements: Tumor measurements over time
- Clinical Findings: Structured radiology reports
Best Practices
- Use standard coded concepts from DCM, SNOMED-CT, or LOINC
- Include image references when findings relate to specific images
- Follow IHE profiles for specific use cases (e.g., dose reporting)
- Use Comprehensive SR for complex reports with multiple content types
- Validate SR structure against the DICOM standard
Conclusion
DICOM Structured Reports provide a powerful way to encode clinical findings in a standardized, machine-readable format. Unlike free-text reports, SR content can be processed automatically, enabling analytics, clinical decision support, and quality reporting.
While creating full-featured Structured Reports can be complex, understanding the basic structure and content types provides a foundation for working with SR in medical imaging applications. Many modern use cases, such as radiation dose tracking and CAD integration, rely heavily on Structured Reports.
Please check out the next tutorial in this series where we cover DICOM Print operations.