DICOM Basics using Java - Waveforms (ECG, EEG)

Introduction

This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore DICOM waveform objects, which store physiological signals such as ECG (electrocardiogram), EEG (electroencephalogram), and other time-series data. This enables integrating waveform data with imaging in PACS.

DICOM waveforms bring cardiology, neurology, and other physiological monitoring data into the same standardized format used for medical imaging.

Prerequisites

Before you begin, ensure you have the following:

  • Java JDK installed (Java 8 or later)
  • PixelMed Java DICOM Toolkit
  • Basic understanding of ECG/physiological signals
  • You can find all the code demonstrated in this tutorial on GitHub here

“The heart has its reasons which reason knows not.” ~ Blaise Pascal

The Theory Behind DICOM Waveforms

DICOM Waveforms extend medical imaging standards into the realm of physiological signal acquisition. This unification addresses a long-standing fragmentation in healthcare IT where ECG systems, EEG systems, and imaging systems existed in isolated silos.

The Signal vs. Image Dichotomy

Medical data fundamentally divides into spatial (images) and temporal (signals) domains:

  • Images: 2D/3D spatial data, discrete pixels, anatomy-focused
  • Signals: 1D time-series, continuous sampling, physiology-focused

Despite this difference, both have the same workflow requirements: patient association, study organization, archival storage, and cross-enterprise sharing. DICOM Waveforms apply imaging workflows to signal data.

The Multi-Channel Data Model

Physiological signals are inherently multi-channel:

  • 12-Lead ECG: 12 simultaneous electrical channels from different body positions
  • EEG: 21+ channels in standard 10-20 electrode placement
  • Polysomnography: ECG + EEG + EMG + respiratory + SpO2 channels

DICOM's Waveform Sequence stores all channels with shared timing, individual calibration, and channel-specific source identification codes. The Channel Definition Sequence provides metadata for each channel's physical meaning and scaling.

Sampling Theory Considerations

Waveform data acquisition must respect the Nyquist-Shannon sampling theorem: to accurately represent a signal, sampling frequency must exceed twice the highest frequency component:

  • ECG: ~100 Hz bandwidth → ≥500 Hz sampling typical
  • EEG: ~40 Hz bandwidth → ≥256 Hz sampling typical
  • EMG: ~500 Hz bandwidth → ≥1000 Hz sampling typical

DICOM stores the Sampling Frequency attribute, enabling receivers to correctly reconstruct the continuous signal from discrete samples.

The Lead Identification Challenge

For ECG data to be clinically useful, each channel must be unambiguously identified. Is this channel Lead I or Lead V1? DICOM addresses this through the Channel Source Sequence using standardized codes (MDC, medical device communication codes):

  • Lead I: MDC_ECG_LEAD_I
  • Lead II: MDC_ECG_LEAD_II
  • Lead V1: MDC_ECG_LEAD_V1

This coded identification enables automated analysis software to correctly interpret lead-specific patterns (e.g., inferior STEMI shows ST elevation in leads II, III, aVF).

Waveform SOP Classes

SOP ClassUIDUse Case
12-Lead ECG1.2.840.10008.5.1.4.1.1.9.1.1Standard 12-lead ECG
General ECG1.2.840.10008.5.1.4.1.1.9.1.2Other ECG formats
Ambulatory ECG1.2.840.10008.5.1.4.1.1.9.1.3Holter monitoring
Hemodynamic1.2.840.10008.5.1.4.1.1.9.2.1Blood pressure, cardiac output
Basic Cardiac EP1.2.840.10008.5.1.4.1.1.9.3.1Electrophysiology studies
General Audio1.2.840.10008.5.1.4.1.1.9.4.1Audio waveforms
Arterial Pulse1.2.840.10008.5.1.4.1.1.9.5.1Pulse waveforms
Respiratory1.2.840.10008.5.1.4.1.1.9.6.1Breathing patterns

Waveform IOD Structure

Waveform Sequence (5400,0100)
  +-- Number of Channels (003A,0005)
  +-- Number of Samples (003A,0010)
  +-- Sampling Frequency (003A,001A)
  +-- Channel Definition Sequence (003A,0200)
  |     +-- Channel Source
  |     +-- Channel Sensitivity
  |     +-- Channel Sensitivity Units
  +-- Waveform Data (5400,1010)

Creating a 12-Lead ECG

package com.saravanansubramanian.dicom.pixelmedtutorial;

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

import com.pixelmed.dicom.*;

public class CreateEcgWaveformDemo {

    public static void main(String[] args) {

        try {

            System.out.println("=== DICOM ECG Waveform Creation ===\n");

            String outputFile = "C:\\temp\\ecg_waveform.dcm";

            // Generate sample ECG data (10 seconds at 500 Hz)
            short[] ecgData = generateSampleEcgData(5000, 500.0);

            // Create the waveform DICOM object
            AttributeList list = create12LeadEcg(
                ecgData,
                500.0,  // Sampling frequency (Hz)
                "Doe^John",
                "PAT123",
                "19700101",
                "M"
            );

            // Add file meta information
            FileMetaInformation.addFileMetaInformation(
                list,
                TransferSyntax.ExplicitVRLittleEndian,
                "ECG_SOURCE"
            );

            // Write the file
            list.write(outputFile);

            System.out.println("ECG Waveform created: " + outputFile);
            System.out.println("Duration: 10 seconds");
            System.out.println("Sampling Rate: 500 Hz");
            System.out.println("Channels: 12 leads");

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

    private static AttributeList create12LeadEcg(
            short[] ecgData,
            double samplingFrequency,
            String patientName,
            String patientId,
            String patientBirthDate,
            String patientSex) 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 sopClass = new UniqueIdentifierAttribute(
            TagFromName.SOPClassUID);
        sopClass.addValue(SOPClass.TwelveLeadECGStorage);
        list.put(sopClass);

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

        // === Patient Module ===
        Attribute patNameAttr = new PersonNameAttribute(
            TagFromName.PatientName);
        patNameAttr.addValue(patientName);
        list.put(patNameAttr);

        Attribute patIdAttr = new LongStringAttribute(TagFromName.PatientID);
        patIdAttr.addValue(patientId);
        list.put(patIdAttr);

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

        // === Waveform Sequence ===
        SequenceAttribute waveformSeq = new SequenceAttribute(
            TagFromName.WaveformSequence);
        AttributeList waveformItem = new AttributeList();

        // Number of channels (12-lead ECG)
        Attribute numChannels = new UnsignedShortAttribute(
            TagFromName.NumberOfWaveformChannels);
        numChannels.addValue(12);
        waveformItem.put(numChannels);

        // Number of samples per channel
        int samplesPerChannel = ecgData.length / 12;
        Attribute numSamples = new UnsignedLongAttribute(
            TagFromName.NumberOfWaveformSamples);
        numSamples.addValue(samplesPerChannel);
        waveformItem.put(numSamples);

        // Sampling frequency
        Attribute sampFreq = new DecimalStringAttribute(
            TagFromName.SamplingFrequency);
        sampFreq.addValue(String.valueOf(samplingFrequency));
        waveformItem.put(sampFreq);

        // Waveform bits allocated
        Attribute bitsAlloc = new UnsignedShortAttribute(
            TagFromName.WaveformBitsAllocated);
        bitsAlloc.addValue(16);
        waveformItem.put(bitsAlloc);

        // Waveform sample interpretation
        Attribute sampleInterp = new CodeStringAttribute(
            TagFromName.WaveformSampleInterpretation);
        sampleInterp.addValue("SS"); // Signed Short
        waveformItem.put(sampleInterp);

        // Add channel definitions and waveform data...
        // (See full code example on GitHub)

        waveformSeq.addItem(waveformItem);
        list.put(waveformSeq);

        return list;
    }
}

12-Lead ECG Leads

Lead TypeLeadsDescription
LimbI, II, IIIStandard limb leads
AugmentedaVR, aVL, aVFAugmented unipolar leads
PrecordialV1-V6Chest leads

Channel Definition Sequence

// === Channel Definition Sequence ===
SequenceAttribute channelDefSeq = new SequenceAttribute(
    TagFromName.ChannelDefinitionSequence);

// Standard 12-lead names
String[] leadNames = {"I", "II", "III", "aVR", "aVL", "aVF",
    "V1", "V2", "V3", "V4", "V5", "V6"};

for (int i = 0; i < 12; i++) {
    AttributeList channelItem = new AttributeList();

    // Channel Source Sequence (identifies the lead)
    SequenceAttribute channelSourceSeq = new SequenceAttribute(
        TagFromName.ChannelSourceSequence);
    AttributeList sourceItem = new AttributeList();

    Attribute codeValue = new ShortStringAttribute(TagFromName.CodeValue);
    codeValue.addValue("5.6.3-9-" + (i + 1)); // MDC codes for ECG leads
    sourceItem.put(codeValue);

    Attribute codingScheme = new ShortStringAttribute(
        TagFromName.CodingSchemeDesignator);
    codingScheme.addValue("MDC");
    sourceItem.put(codingScheme);

    Attribute codeMeaning = new LongStringAttribute(TagFromName.CodeMeaning);
    codeMeaning.addValue("Lead " + leadNames[i]);
    sourceItem.put(codeMeaning);

    channelSourceSeq.addItem(sourceItem);
    channelItem.put(channelSourceSeq);

    // Channel sensitivity
    Attribute sensitivity = new DecimalStringAttribute(
        TagFromName.ChannelSensitivity);
    sensitivity.addValue("1.0");
    channelItem.put(sensitivity);

    // Channel sensitivity units (microvolts)
    // ...

    channelDefSeq.addItem(channelItem);
}

waveformItem.put(channelDefSeq);

Key Waveform Attributes

TagNameDescription
(5400,0100)Waveform SequenceContains waveform data
(003A,0005)Number of Waveform ChannelsChannel count
(003A,0010)Number of Waveform SamplesSamples per channel
(003A,001A)Sampling FrequencySamples per second (Hz)
(5400,0102)Waveform Bits Allocated8 or 16 bits
(5400,1004)Waveform Bits StoredActual bits used
(5400,1006)Waveform Sample InterpretationSS, US, SB, UB, etc.
(003A,0200)Channel Definition SequencePer-channel metadata
(5400,1010)Waveform DataRaw waveform samples

Sample Interpretation Values

ValueDescription
SSSigned 16-bit integer
USUnsigned 16-bit integer
SBSigned 8-bit integer
UBUnsigned 8-bit integer

Common Use Cases

  • Cardiology: 12-lead ECG, stress tests, Holter monitoring
  • Neurology: EEG recordings
  • Critical Care: Hemodynamic monitoring
  • Surgery: Intraoperative monitoring
  • Research: Clinical trials, physiological studies

Integration with PACS

DICOM waveforms can be:

  • Stored in PACS alongside imaging studies
  • Linked to patient records via Study/Patient IDs
  • Viewed in DICOM workstations that support waveform display
  • Queried and retrieved using standard DICOM services

Conclusion

DICOM waveform objects extend the DICOM standard beyond imaging to include physiological signals like ECG and EEG. This enables unified storage and retrieval of all patient diagnostic data within PACS infrastructure.

Understanding waveform IOD structure, channel definitions, and sampling parameters is essential for building applications that integrate cardiology, neurology, and other physiological monitoring systems with PACS. In the next tutorial in this series, I will cover multi-modality DICOM attributes for CT, MR, US, and other imaging types. See you then!