DICOM Basics using .NET and C# - Private Tags
Introduction
This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore DICOM private tags, which allow vendors to store proprietary information in DICOM objects. Understanding private tags is essential when working with data from different imaging equipment manufacturers.
Private tags use odd group numbers and require a Private Creator Identification element to reserve a block of elements. While they enable vendor-specific functionality, they also present challenges for interoperability and de-identification.
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
“The measure of intelligence is the ability to change.” ~ Albert Einstein
The Theory Behind Private Tags
Private tags exist because the DICOM standard, despite its comprehensiveness, cannot anticipate every vendor's needs. Medical imaging equipment generates rich data beyond what standard tags capture: proprietary reconstruction algorithms, scanner-specific calibration data, and features that may later become standardized. Private tags provide an extension mechanism that allows innovation while preserving interoperability.
The odd-group convention creates a clear namespace separation. Standard DICOM tags use even group numbers (0008, 0010, 0018, etc.). Private tags use odd group numbers (0009, 0011, 0019, etc.). This simple rule means parsers can immediately identify whether a tag is standard or proprietary without consulting any dictionary.
The Private Creator mechanism solves the namespace collision problem. Without it, two vendors could both use tag (0009,1000) for completely different purposes. The Private Creator element reserves a block of 256 elements within a group for a specific vendor. By checking the Private Creator string (like "SIEMENS MR HEADER"), systems know which vendor's dictionary to consult for interpretation.
From an information preservation perspective, private tags present a dilemma. When DICOM data moves between systems, private tags may be stripped, modified, or misinterpreted. A PACS might not understand Siemens MR-specific parameters, but removing them means losing potentially valuable information. Best practice is to preserve private tags even when their meaning is unknown, allowing systems that do understand them to benefit.
For de-identification, private tags are particularly challenging. Since their structure is vendor-specific, you cannot know whether a private tag contains PHI without vendor documentation. A conservative approach removes all private tags during anonymization, but this may eliminate important research data. More sophisticated approaches use vendor-specific profiles that identify which private tags are safe to keep.
Private Tag Structure
Private tags follow a specific structure:
| Component | Format | Example |
|---|---|---|
| Standard Tag | (gggg,eeee) with even gggg | (0010,0010) |
| Private Tag | (gggg,eeee) with odd gggg | (0009,1001) |
| Private Creator | (gggg,00xx) where xx is block | (0009,0010) |
| Private Data | (gggg,xxyy) within block | (0009,1000-10FF) |
Step 1 of 4: Understanding Private Tag Blocks
Private tags are organized into blocks within odd group numbers:
using System;
using System.Diagnostics;
using FellowOakDicom;
namespace DicomPrivateTags
{
public class Program
{
public static void Main(string[] args)
{
try
{
LogToDebugConsole("=== DICOM Private Tags Demo ===");
LogToDebugConsole("");
DemonstratePrivateTagStructure();
DemonstrateCreatingPrivateTags();
DemonstrateVendorPrivateTags();
}
catch (Exception e)
{
LogToDebugConsole($"Error: {e.Message}");
}
}
private static void DemonstratePrivateTagStructure()
{
LogToDebugConsole("--- Private Tag Structure ---");
LogToDebugConsole("");
LogToDebugConsole("Private tag blocks:");
LogToDebugConsole("");
LogToDebugConsole(" Group 0009, Block 10:");
LogToDebugConsole(" (0009,0010) = Private Creator Identification");
LogToDebugConsole(" (0009,1000) through (0009,10FF) = Private data elements");
LogToDebugConsole("");
LogToDebugConsole(" Group 0009, Block 11:");
LogToDebugConsole(" (0009,0011) = Private Creator Identification");
LogToDebugConsole(" (0009,1100) through (0009,11FF) = Private data elements");
LogToDebugConsole("");
LogToDebugConsole("Example from a GE scanner:");
LogToDebugConsole(" (0009,0010) LO \"GEMS_IDEN_01\" <- Creator ID");
LogToDebugConsole(" (0009,1001) LO \"CT01\" <- Product ID");
LogToDebugConsole(" (0009,1002) SH \"CT Lightspeed\" <- Scanner model");
}
private static void LogToDebugConsole(string message)
{
Debug.WriteLine(message);
}
}
}
Step 2 of 4: Creating Private Tags
Here's how to properly create private tags with fo-dicom:
private static void DemonstrateCreatingPrivateTags()
{
LogToDebugConsole("--- Creating Private Tags ---");
LogToDebugConsole("");
var dataset = new DicomDataset();
// Add standard attributes first
dataset.Add(DicomTag.PatientName, "Doe^John");
dataset.Add(DicomTag.PatientID, "PAT123");
//---------------------------------------------------------------
// Step 1: Reserve a private block
// Use group 0009, block 10 (element 0010)
//---------------------------------------------------------------
var creatorTag = new DicomTag(0x0009, 0x0010);
dataset.Add(creatorTag, "MY_APPLICATION");
LogToDebugConsole("Step 1: Reserve private block");
LogToDebugConsole(" Tag: (0009,0010)");
LogToDebugConsole(" Creator: MY_APPLICATION");
LogToDebugConsole("");
//---------------------------------------------------------------
// Step 2: Add private data elements
// Elements (0009,1000) through (0009,10FF) are in block 10
//---------------------------------------------------------------
var privateTag1 = new DicomTag(0x0009, 0x1000);
dataset.Add(privateTag1, "Custom Value 1");
var privateTag2 = new DicomTag(0x0009, 0x1001);
dataset.Add(privateTag2, "Custom Value 2");
LogToDebugConsole("Step 2: Add private data elements");
LogToDebugConsole(" (0009,1000) = \"Custom Value 1\"");
LogToDebugConsole(" (0009,1001) = \"Custom Value 2\"");
LogToDebugConsole("");
//---------------------------------------------------------------
// Step 3: Use another block if needed
//---------------------------------------------------------------
var creator2Tag = new DicomTag(0x0009, 0x0011);
dataset.Add(creator2Tag, "MY_APP_EXTENDED");
var privateTag3 = new DicomTag(0x0009, 0x1100);
dataset.Add(privateTag3, "Extended data");
LogToDebugConsole("Step 3: Use additional block (0009,11xx)");
LogToDebugConsole(" (0009,0011) Creator: MY_APP_EXTENDED");
LogToDebugConsole(" (0009,1100) = \"Extended data\"");
LogToDebugConsole("");
// Read back to verify
LogToDebugConsole("Reading back private tags:");
LogToDebugConsole($" (0009,0010) = {dataset.GetSingleValueOrDefault(creatorTag, "")}");
LogToDebugConsole($" (0009,1000) = {dataset.GetSingleValueOrDefault(privateTag1, "")}");
LogToDebugConsole($" (0009,1001) = {dataset.GetSingleValueOrDefault(privateTag2, "")}");
}
Step 3 of 4: Reading Private Tags Safely
When reading private tags, always verify the creator first:
public static void ReadPrivateTagsSafely(DicomDataset dataset)
{
// Check for our specific creator first
var creatorTag = new DicomTag(0x0009, 0x0010);
var creator = dataset.GetSingleValueOrDefault<string>(creatorTag, null);
if (creator == "MY_APPLICATION")
{
// Safe to read our private data
var privateTag = new DicomTag(0x0009, 0x1000);
var data = dataset.GetSingleValueOrDefault<string>(privateTag, null);
if (data != null)
{
LogToDebugConsole($"Found our private data: {data}");
}
}
else
{
LogToDebugConsole($"Different creator: {creator ?? "(none)"}");
// Don't assume the structure of unknown private tags
}
}
public static void ListAllPrivateTags(DicomDataset dataset)
{
LogToDebugConsole("Scanning for private tags...");
foreach (var item in dataset)
{
// Private tags have odd group numbers
if (item.Tag.Group % 2 == 1)
{
var value = item is DicomElement element
? element.Get<string>()
: "(sequence or binary)";
LogToDebugConsole($" {item.Tag}: {value}");
}
}
}
Step 4 of 4: Common Vendor Private Tags
Here are private tags from major vendors:
private static void DemonstrateVendorPrivateTags()
{
LogToDebugConsole("--- Common Vendor Private Tags ---");
LogToDebugConsole("");
LogToDebugConsole("GE Healthcare:");
LogToDebugConsole(" Creator: GEMS_IDEN_01, GEMS_ACQU_01, GEMS_IMAG_01");
LogToDebugConsole(" Groups: 0009, 0019, 0021, 0023, 0025, 0027, 0043, 0045");
LogToDebugConsole(" Contains: Scanner parameters, reconstruction settings");
LogToDebugConsole("");
LogToDebugConsole("Siemens Healthineers:");
LogToDebugConsole(" Creator: SIEMENS MR HEADER, SIEMENS CT VA0, etc.");
LogToDebugConsole(" Groups: 0019, 0021, 0029, 0051, 7FE1");
LogToDebugConsole(" Contains: Sequence parameters, CSA headers (MR)");
LogToDebugConsole("");
LogToDebugConsole("Philips Healthcare:");
LogToDebugConsole(" Creator: PHILIPS MR, Philips Imaging DD 001");
LogToDebugConsole(" Groups: 2001, 2005, 7053");
LogToDebugConsole(" Contains: Scanning parameters, Private Pixel Data");
LogToDebugConsole("");
LogToDebugConsole("Canon (formerly Toshiba):");
LogToDebugConsole(" Creator: TOSHIBA_MEC_CT_01, TOSHIBA_MEC_MR_01");
LogToDebugConsole(" Groups: 7005, 700D");
LogToDebugConsole(" Contains: Acquisition parameters");
}
| Vendor | Typical Groups | Creator Examples |
|---|---|---|
| GE Healthcare | 0009, 0019, 0043 | GEMS_IDEN_01, GEMS_ACQU_01 |
| Siemens | 0019, 0021, 0029 | SIEMENS MR HEADER |
| Philips | 2001, 2005 | PHILIPS MR |
| Canon/Toshiba | 7005, 700D | TOSHIBA_MEC_CT_01 |
Privacy and Anonymization Considerations
Private tags present special challenges for de-identification:
public static void HandlePrivateTagsForAnonymization(DicomDataset dataset,
bool removeAll = true)
{
if (removeAll)
{
// Option 1: Remove all private tags (safest for de-identification)
var tagsToRemove = dataset
.Where(item => item.Tag.Group % 2 == 1)
.Select(item => item.Tag)
.ToList();
foreach (var tag in tagsToRemove)
{
dataset.Remove(tag);
}
LogToDebugConsole($"Removed {tagsToRemove.Count} private tags");
}
else
{
// Option 2: Selectively keep known safe private tags
// Requires knowledge of what each vendor's tags contain
// Risk: May inadvertently keep PHI-containing tags
}
}
Best Practices
DO:
- Always register a Private Creator Identification
- Use unique, identifiable creator names
- Document your private tag definitions
- Use appropriate VRs for your data
- Check if a standard tag already exists first
DON’T:
- Use private tags without a creator identification
- Assume private tags will survive all DICOM transfers
- Store PHI in private tags without proper handling
- Use even group numbers for private data
- Conflict with known vendor private tags
Resources for Private Tags
- DICOM Innolitics - Comprehensive tag browser
- DICOM Library - Private tag documentation
- DCMTK private tag definitions
- Vendor-specific DICOM conformance statements
Conclusion
Private tags enable vendors to include proprietary information in DICOM objects, but they require careful handling. Always use proper creator identification, verify creators before reading private data, and consider the implications for de-identification workflows.
When building applications that process DICOM data from multiple sources, be prepared to encounter various private tag structures. While you may not need to interpret all vendor-specific data, understanding the private tag mechanism helps you handle these elements appropriately.
Please check out the next tutorial in this series where we cover DICOM digital signatures.