DICOM Basics using .NET and C# - Key Object Selection (KOS)
Introduction
This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore DICOM Key Object Selection (KOS) documents, which provide a standardized way to mark and reference significant images within a study.
KOS documents are based on the DICOM Structured Report format and serve multiple purposes including teaching file management, quality rejection workflows, and creating image manifests for data exchange.
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 longest distance between two places.” ~ Tennessee Williams
The Theory Behind Key Object Selection
A modern CT study may contain hundreds or thousands of images - every slice, every phase, multiple reconstructions. For many clinical purposes, only a few images are truly significant: the one showing the lesion, the key finding, the image that changed the diagnosis. Key Object Selection addresses the signal-to-noise problem in medical imaging: how do you highlight what matters in a sea of data?
KOS implements a manifest pattern: a lightweight document that points to significant objects without duplicating them. The KOS itself is tiny (a few KB), containing only references (SOP Instance UIDs) and purpose codes. This enables efficient transmission of "which images matter" without resending the images themselves. A teaching file export can query for KOS documents marked "For Teaching" and retrieve just those referenced images.
The purpose codes (CID 7010) provide standardized semantics for why images were selected. This isn't free-form text that requires human interpretation - it's machine-readable codes that enable automated workflows. A quality rejection workflow might query for all KOS with code 113001 (Rejected for Quality Reasons), automatically flagging those images for review or hiding them from clinical display.
The rejection flag pattern deserves special attention. DICOM doesn't have a "delete" operation - images, once stored, are part of the permanent record. But some images shouldn't be displayed clinically: motion artifacts, wrong patient identification, test acquisitions. KOS with rejection codes provides a "soft delete" mechanism: the image remains in the archive for audit purposes but is marked as rejected, signaling viewers to hide it from routine display.
KOS is built on the Structured Report foundation, which explains its structure. It uses SR concepts like Content Sequence and coded concept names. This wasn't arbitrary - it means KOS benefits from SR infrastructure: viewers that display SR can display KOS, and the same parsing code handles both. It's an example of DICOM's composable architecture.
Understanding Key Object Selection
Key Object Selection documents have the following characteristics:
| Attribute | Value |
|---|---|
| SOP Class UID | 1.2.840.10008.5.1.4.1.1.88.59 |
| Modality | KO (Key Object) |
| Format | Based on DICOM Structured Report |
| Content | References to selected images + purpose codes |
Common Use Cases
- Teaching Files: Mark exemplary cases for educational purposes
- Quality Assurance: Flag images for review or rejection
- Clinical Highlights: Identify key findings for referring physicians
- Study Manifest: List images for transmission or verification
- Research: Mark images included in research datasets
Step 1 of 4: Creating a Key Object Selection Document
Here's how to create a KOS document that references selected images:
using System;
using System.Diagnostics;
using System.IO;
using FellowOakDicom;
namespace DicomKeyObjectSelection
{
public class Program
{
private static readonly string OutputPath =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Output");
public static void Main(string[] args)
{
try
{
LogToDebugConsole("=== DICOM Key Object Selection Demo ===");
if (!Directory.Exists(OutputPath))
{
Directory.CreateDirectory(OutputPath);
}
CreateKeyObjectSelectionDemo();
}
catch (Exception e)
{
LogToDebugConsole($"Error: {e.Message}");
}
}
private static void CreateKeyObjectSelectionDemo()
{
var dataset = new DicomDataset();
string currentDate = DateTime.Now.ToString("yyyyMMdd");
string currentTime = DateTime.Now.ToString("HHmmss");
// Referenced images (example UIDs - use actual UIDs from your study)
string referencedStudyUID = "1.2.3.4.5.6.7.8.9";
string referencedSeriesUID = "1.2.3.4.5.6.7.8.9.1";
string[] referencedSOPInstanceUIDs = {
"1.2.3.4.5.6.7.8.9.1.1",
"1.2.3.4.5.6.7.8.9.1.5",
"1.2.3.4.5.6.7.8.9.1.10"
};
//---------------------------------------------------------------
// SOP Common Module
//---------------------------------------------------------------
dataset.Add(DicomTag.SOPClassUID, DicomUID.KeyObjectSelectionDocumentStorage);
dataset.Add(DicomTag.SOPInstanceUID, DicomUID.Generate());
//---------------------------------------------------------------
// Patient Module
//---------------------------------------------------------------
dataset.Add(DicomTag.PatientName, "Doe^John");
dataset.Add(DicomTag.PatientID, "PAT123");
dataset.Add(DicomTag.PatientBirthDate, "19700101");
dataset.Add(DicomTag.PatientSex, "M");
//---------------------------------------------------------------
// General Study Module (same as referenced study)
//---------------------------------------------------------------
dataset.Add(DicomTag.StudyInstanceUID, referencedStudyUID);
dataset.Add(DicomTag.StudyDate, currentDate);
dataset.Add(DicomTag.StudyTime, currentTime);
dataset.Add(DicomTag.AccessionNumber, "ACC123");
dataset.Add(DicomTag.ReferringPhysicianName, "Smith^Jane^Dr");
dataset.Add(DicomTag.StudyID, "STUDY001");
//---------------------------------------------------------------
// KOS Series Module
//---------------------------------------------------------------
dataset.Add(DicomTag.Modality, "KO"); // Key Object
dataset.Add(DicomTag.SeriesInstanceUID, DicomUID.Generate());
dataset.Add(DicomTag.SeriesNumber, "999");
dataset.Add(DicomTag.SeriesDescription, "Key Object Selection");
//---------------------------------------------------------------
// SR Document General Module
//---------------------------------------------------------------
dataset.Add(DicomTag.InstanceNumber, "1");
dataset.Add(DicomTag.ContentDate, currentDate);
dataset.Add(DicomTag.ContentTime, currentTime);
// Add referenced series and images
AddReferencedImages(dataset, referencedSeriesUID, referencedSOPInstanceUIDs);
// Add purpose code
AddPurposeCode(dataset, "113000", "Of Interest");
//---------------------------------------------------------------
// Save the file
//---------------------------------------------------------------
string outputFile = Path.Combine(OutputPath, "key_object_selection.dcm");
var dicomFile = new DicomFile(dataset);
dicomFile.Save(outputFile);
LogToDebugConsole($"Key Object Selection created: {outputFile}");
LogToDebugConsole($"Number of referenced images: {referencedSOPInstanceUIDs.Length}");
}
private static void LogToDebugConsole(string message)
{
Debug.WriteLine(message);
}
}
}
Step 2 of 4: Adding Referenced Images
The core of a KOS document is the list of referenced images:
private static void AddReferencedImages(DicomDataset dataset,
string seriesUID, string[] sopInstanceUIDs)
{
//---------------------------------------------------------------
// Referenced Series Sequence
//---------------------------------------------------------------
var referencedSeriesSeq = new DicomSequence(DicomTag.ReferencedSeriesSequence);
var seriesItem = new DicomDataset();
seriesItem.Add(DicomTag.SeriesInstanceUID, seriesUID);
//---------------------------------------------------------------
// Referenced SOP Sequence (images within the series)
//---------------------------------------------------------------
var referencedSOPSeq = new DicomSequence(DicomTag.ReferencedSOPSequence);
foreach (var instanceUID in sopInstanceUIDs)
{
var sopItem = new DicomDataset();
sopItem.Add(DicomTag.ReferencedSOPClassUID, DicomUID.CTImageStorage);
sopItem.Add(DicomTag.ReferencedSOPInstanceUID, instanceUID);
referencedSOPSeq.Items.Add(sopItem);
}
seriesItem.Add(referencedSOPSeq);
referencedSeriesSeq.Items.Add(seriesItem);
dataset.Add(referencedSeriesSeq);
}
Step 3 of 4: Adding Purpose Codes
Purpose codes from CID 7010 indicate why images were selected:
private static void AddPurposeCode(DicomDataset dataset,
string codeValue, string codeMeaning)
{
//---------------------------------------------------------------
// Content Sequence (SR structure for KOS)
//---------------------------------------------------------------
var contentSeq = new DicomSequence(DicomTag.ContentSequence);
var purposeItem = new DicomDataset();
purposeItem.Add(DicomTag.RelationshipType, "HAS CONCEPT MOD");
purposeItem.Add(DicomTag.ValueType, "CODE");
// Concept Name Code Sequence - What this item represents
var conceptNameSeq = new DicomSequence(DicomTag.ConceptNameCodeSequence);
var conceptNameItem = new DicomDataset();
conceptNameItem.Add(DicomTag.CodeValue, "113012");
conceptNameItem.Add(DicomTag.CodingSchemeDesignator, "DCM");
conceptNameItem.Add(DicomTag.CodeMeaning, "Key Object Description");
conceptNameSeq.Items.Add(conceptNameItem);
purposeItem.Add(conceptNameSeq);
// Concept Code Sequence - The actual purpose code
var conceptCodeSeq = new DicomSequence(DicomTag.ConceptCodeSequence);
var conceptCodeItem = new DicomDataset();
conceptCodeItem.Add(DicomTag.CodeValue, codeValue);
conceptCodeItem.Add(DicomTag.CodingSchemeDesignator, "DCM");
conceptCodeItem.Add(DicomTag.CodeMeaning, codeMeaning);
conceptCodeSeq.Items.Add(conceptCodeItem);
purposeItem.Add(conceptCodeSeq);
contentSeq.Items.Add(purposeItem);
dataset.Add(contentSeq);
}
Step 4 of 4: Purpose Codes Reference (CID 7010)
Here are the common purpose codes for Key Object Selection:
| Code Value | Code Meaning | Use Case |
|---|---|---|
| 113000 | Of Interest | General selection |
| 113001 | Rejected for Quality Reasons | Quality rejection |
| 113002 | For Referring Provider | Key findings for referrer |
| 113003 | For Surgery | Pre-surgical planning |
| 113004 | For Teaching | Teaching files |
| 113005 | For Conference | Case presentation |
| 113006 | For Therapy | Treatment planning |
| 113007 | For Patient | Patient portal/CD |
| 113008 | For Peer Review | Quality review |
| 113009 | For Research | Research datasets |
| 113010 | Quality Issue | QA flagging |
| 113013 | Best In Set | Representative images |
| 113018 | For Printing | Print selection |
| 113020 | For Report Attachment | Report images |
| 113030 | Manifest | Complete listing |
| 113031 | Signed Manifest | Verified complete listing |
Rejection Reason Codes (CID 7011)
For quality rejection workflows:
| Code Value | Code Meaning |
|---|---|
| 113001 | Rejected for Quality Reasons |
| 113037 | Rejected for Patient Safety |
| 113038 | Incorrect Modality Worklist Entry |
| 113039 | Data Retention Policy Expired |
Reading Key Object Selection Documents
To read and process KOS documents:
public static void ReadKeyObjectSelection(string kosFilePath)
{
var file = DicomFile.Open(kosFilePath);
var dataset = file.Dataset;
// Verify it's a KOS document
var sopClass = dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, "");
if (sopClass != DicomUID.KeyObjectSelectionDocumentStorage.UID)
{
LogToDebugConsole("Not a Key Object Selection document");
return;
}
LogToDebugConsole("Key Object Selection Document");
LogToDebugConsole($" Modality: {dataset.GetSingleValueOrDefault(DicomTag.Modality, "")}");
// Read referenced series
var referencedSeriesSeq = dataset.GetSequence(DicomTag.ReferencedSeriesSequence);
if (referencedSeriesSeq != null)
{
foreach (var seriesItem in referencedSeriesSeq.Items)
{
var seriesUID = seriesItem.GetSingleValueOrDefault(
DicomTag.SeriesInstanceUID, "");
LogToDebugConsole($" Referenced Series: {seriesUID}");
// Read referenced images
var sopSeq = seriesItem.GetSequence(DicomTag.ReferencedSOPSequence);
if (sopSeq != null)
{
foreach (var sopItem in sopSeq.Items)
{
var instanceUID = sopItem.GetSingleValueOrDefault(
DicomTag.ReferencedSOPInstanceUID, "");
LogToDebugConsole($" Referenced Image: {instanceUID}");
}
}
}
}
// Read purpose codes from Content Sequence
var contentSeq = dataset.GetSequence(DicomTag.ContentSequence);
if (contentSeq != null)
{
foreach (var item in contentSeq.Items)
{
var codeSeq = item.GetSequence(DicomTag.ConceptCodeSequence);
if (codeSeq != null && codeSeq.Items.Count > 0)
{
var codeMeaning = codeSeq.Items[0].GetSingleValueOrDefault(
DicomTag.CodeMeaning, "");
LogToDebugConsole($" Purpose: {codeMeaning}");
}
}
}
}
Practical Workflow Examples
Teaching File Export:
1. Radiologist marks interesting cases in viewer
2. PACS creates KOS document with "For Teaching" code
3. Export system queries for KOS documents
4. System retrieves images referenced in KOS
5. Images exported to teaching file system
Quality Rejection:
1. Technologist identifies motion-blurred image
2. System creates KOS with "Rejected for Quality" code
3. PACS marks referenced image as rejected
4. Image hidden from clinical display
5. Audit trail maintained via KOS
Best Practices
- Use standard purpose codes: Enables interoperability with other systems
- Include all required modules: Patient, Study, Series, and SOP Common
- Match Study UID: KOS should reference images within same study
- Add meaningful descriptions: Use Series Description and Content
- Consider multiple series: One KOS can reference images from multiple series
Conclusion
Key Object Selection documents provide a powerful mechanism for flagging and organizing significant images within DICOM studies. By leveraging standard purpose codes and the structured report format, KOS enables workflows ranging from teaching file management to quality rejection.
Understanding KOS is essential for building systems that integrate with PACS for image selection, export, and quality management workflows. The standardized format ensures interoperability across different vendors and systems.
Please check out the next tutorial in this series where we cover DICOM Segmentation objects.