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:

AttributeValue
SOP Class UID1.2.840.10008.5.1.4.1.1.88.59
ModalityKO (Key Object)
FormatBased on DICOM Structured Report
ContentReferences 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 ValueCode MeaningUse Case
113000Of InterestGeneral selection
113001Rejected for Quality ReasonsQuality rejection
113002For Referring ProviderKey findings for referrer
113003For SurgeryPre-surgical planning
113004For TeachingTeaching files
113005For ConferenceCase presentation
113006For TherapyTreatment planning
113007For PatientPatient portal/CD
113008For Peer ReviewQuality review
113009For ResearchResearch datasets
113010Quality IssueQA flagging
113013Best In SetRepresentative images
113018For PrintingPrint selection
113020For Report AttachmentReport images
113030ManifestComplete listing
113031Signed ManifestVerified complete listing

Rejection Reason Codes (CID 7011)

For quality rejection workflows:

Code ValueCode Meaning
113001Rejected for Quality Reasons
113037Rejected for Patient Safety
113038Incorrect Modality Worklist Entry
113039Data 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.