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 Class | UID | Use Case |
|---|---|---|
| 12-Lead ECG | 1.2.840.10008.5.1.4.1.1.9.1.1 | Standard 12-lead ECG |
| General ECG | 1.2.840.10008.5.1.4.1.1.9.1.2 | Other ECG formats |
| Ambulatory ECG | 1.2.840.10008.5.1.4.1.1.9.1.3 | Holter monitoring |
| Hemodynamic | 1.2.840.10008.5.1.4.1.1.9.2.1 | Blood pressure, cardiac output |
| Basic Cardiac EP | 1.2.840.10008.5.1.4.1.1.9.3.1 | Electrophysiology studies |
| General Audio | 1.2.840.10008.5.1.4.1.1.9.4.1 | Audio waveforms |
| Arterial Pulse | 1.2.840.10008.5.1.4.1.1.9.5.1 | Pulse waveforms |
| Respiratory | 1.2.840.10008.5.1.4.1.1.9.6.1 | Breathing 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 Type | Leads | Description |
|---|---|---|
| Limb | I, II, III | Standard limb leads |
| Augmented | aVR, aVL, aVF | Augmented unipolar leads |
| Precordial | V1-V6 | Chest 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
| Tag | Name | Description |
|---|---|---|
| (5400,0100) | Waveform Sequence | Contains waveform data |
| (003A,0005) | Number of Waveform Channels | Channel count |
| (003A,0010) | Number of Waveform Samples | Samples per channel |
| (003A,001A) | Sampling Frequency | Samples per second (Hz) |
| (5400,0102) | Waveform Bits Allocated | 8 or 16 bits |
| (5400,1004) | Waveform Bits Stored | Actual bits used |
| (5400,1006) | Waveform Sample Interpretation | SS, US, SB, UB, etc. |
| (003A,0200) | Channel Definition Sequence | Per-channel metadata |
| (5400,1010) | Waveform Data | Raw waveform samples |
Sample Interpretation Values
| Value | Description |
|---|---|
| SS | Signed 16-bit integer |
| US | Unsigned 16-bit integer |
| SB | Signed 8-bit integer |
| UB | Unsigned 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!