Implementing best practices ensures reliable, efficient DICOM applications.
Connection Pooling
Reuse DICOM associations for improved performance:
public class DicomConnectionPool
{
private readonly ConcurrentQueue<DicomAssociation> availableConnections;
private readonly SemaphoreSlim connectionSemaphore;
private readonly string callingAE;
private readonly string calledAE;
private readonly string host;
private readonly int port;
private readonly int maxConnections;
public DicomConnectionPool(
string callingAE, string calledAE, string host, int port,
int maxConnections = 5)
{
this.callingAE = callingAE;
this.calledAE = calledAE;
this.host = host;
this.port = port;
this.maxConnections = maxConnections;
availableConnections = new ConcurrentQueue<DicomAssociation>();
connectionSemaphore = new SemaphoreSlim(maxConnections, maxConnections);
}
public async Task<DicomAssociation> AcquireConnection()
{
await connectionSemaphore.WaitAsync();
if (availableConnections.TryDequeue(out DicomAssociation association))
{
// Verify connection is still alive
if (association.IsOpen)
{
return association;
}
}
// Create new connection
association = new DicomAssociation
{
CallingAETitle = callingAE,
CalledAETitle = calledAE,
Host = host,
Port = port
};
association.AddPresentationContext(
SOPClasses.CTImageStorage,
DicomTransferSyntax.ExplicitVRLittleEndian
);
association.Open();
return association;
}
public void ReleaseConnection(DicomAssociation association)
{
if (association != null && association.IsOpen)
{
availableConnections.Enqueue(association);
}
connectionSemaphore.Release();
}
public void CloseAll()
{
while (availableConnections.TryDequeue(out DicomAssociation association))
{
if (association.IsOpen)
{
association.Close();
}
}
}
}
// Usage
var pool = new DicomConnectionPool("MY_SCU", "PACS_SCP", "192.168.1.100", 104);
foreach (string file in imageFiles)
{
var association = await pool.AcquireConnection();
try
{
SendImage(association, file);
}
finally
{
pool.ReleaseConnection(association);
}
}
pool.CloseAll();
Memory Management for Large Files
Handle large DICOM files efficiently:
public class EfficientDicomHandler
{
public void ProcessLargeFile(string filePath)
{
// Option 1: Stream pixel data instead of loading entire file
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
DicomDataSet ds = new DicomDataSet();
// Read only metadata, skip pixel data initially
ds.Read(fs, ReadOptions.SkipPixelData);
// Process metadata
ProcessMetadata(ds);
// Load pixel data only if needed
if (NeedsPixelData())
{
fs.Seek(0, SeekOrigin.Begin);
ds.Read(fs);
ProcessPixelData(ds);
}
}
}
public void CompressPixelData(string inputFile, string outputFile)
{
DicomDataSet ds = new DicomDataSet();
ds.Read(inputFile);
// Check if already compressed
if (ds.TransferSyntaxUID != DicomTransferSyntax.ExplicitVRLittleEndian &&
ds.TransferSyntaxUID != DicomTransferSyntax.ImplicitVRLittleEndian)
{
Console.WriteLine("Already compressed");
return;
}
// Re-encode with JPEG 2000 lossless
ds.TransferSyntaxUID = DicomTransferSyntax.JPEG2000Lossless;
ds.Write(outputFile);
// Compare file sizes
long originalSize = new FileInfo(inputFile).Length;
long compressedSize = new FileInfo(outputFile).Length;
double ratio = (double)originalSize / compressedSize;
Console.WriteLine($"Original: {originalSize / (1024 * 1024)} MB");
Console.WriteLine($"Compressed: {compressedSize / (1024 * 1024)} MB");
Console.WriteLine($"Ratio: {ratio:F2}:1");
}
public byte[] ExtractThumbnail(DicomDataSet ds, int maxSize = 256)
{
int rows = int.Parse(ds[DicomTag.Rows]?.Value as string);
int cols = int.Parse(ds[DicomTag.Columns]?.Value as string);
double scale = Math.Min((double)maxSize / rows, (double)maxSize / cols);
int newRows = (int)(rows * scale);
int newCols = (int)(cols * scale);
byte[] pixelData = ds[DicomTag.PixelData]?.Value as byte[];
return ResizeImage(pixelData, rows, cols, newRows, newCols);
}
private void ProcessMetadata(DicomDataSet ds) { /* Implementation */ }
private bool NeedsPixelData() { return true; }
private void ProcessPixelData(DicomDataSet ds) { /* Implementation */ }
private byte[] ResizeImage(byte[] pixels, int origRows, int origCols,
int newRows, int newCols) { /* Implementation */ return null; }
}
Batch Processing
Efficiently process large numbers of files:
public class DicomBatchProcessor
{
private readonly int batchSize = 100;
private readonly int maxParallelism = 4;
public async Task ProcessDirectory(
string directoryPath,
Func<DicomDataSet, Task> processor)
{
var files = Directory.GetFiles(directoryPath, "*.dcm",
SearchOption.AllDirectories);
Console.WriteLine($"Found {files.Length} DICOM files");
var batches = files
.Select((file, index) => new { file, index })
.GroupBy(x => x.index / batchSize)
.Select(g => g.Select(x => x.file).ToList());
int batchNumber = 1;
foreach (var batch in batches)
{
Console.WriteLine($"Processing batch {batchNumber}...");
await ProcessBatch(batch, processor);
batchNumber++;
}
Console.WriteLine("Batch processing complete");
}
private async Task ProcessBatch(
List<string> files,
Func<DicomDataSet, Task> processor)
{
var options = new ParallelOptions
{
MaxDegreeOfParallelism = maxParallelism
};
int successCount = 0;
int errorCount = 0;
await Parallel.ForEachAsync(files, options, async (file, ct) =>
{
try
{
DicomDataSet ds = new DicomDataSet();
ds.Read(file);
await processor(ds);
Interlocked.Increment(ref successCount);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {Path.GetFileName(file)}: {ex.Message}");
Interlocked.Increment(ref errorCount);
}
});
Console.WriteLine($" Success: {successCount}, Errors: {errorCount}");
}
}
// Usage - Anonymization example
var processor = new DicomBatchProcessor();
await processor.ProcessDirectory(@"C:\DicomData", async (ds) =>
{
ds[DicomTag.PatientName].Value = "ANONYMOUS";
ds[DicomTag.PatientID].Value = "ANON" + Guid.NewGuid().ToString("N").Substring(0, 8);
if (ds.Contains(DicomTag.PatientBirthDate))
ds.Remove(DicomTag.PatientBirthDate);
ds[DicomTag.StudyInstanceUID].Value = DicomUID.Generate();
ds[DicomTag.SeriesInstanceUID].Value = DicomUID.Generate();
ds[DicomTag.SOPInstanceUID].Value = DicomUID.Generate();
string outputPath = Path.Combine(@"C:\AnonymizedDicom",
Path.GetFileName(ds.SourceFileName));
ds.Write(outputPath);
});
Error Handling and Retry Logic
Implement robust error handling:
public class RobustDicomClient
{
private readonly int maxRetries = 3;
private readonly int retryDelayMs = 1000;
public async Task<bool> SendWithRetry(
string callingAE, string calledAE, string host, int port,
string filePath)
{
int attempt = 0;
Exception lastException = null;
while (attempt < maxRetries)
{
try
{
attempt++;
Console.WriteLine($"Attempt {attempt} of {maxRetries}");
DicomDataSet dataset = new DicomDataSet();
dataset.Read(filePath);
string sopClassUID = dataset[DicomTag.SOPClassUID]?.Value as string;
DicomAssociation association = new DicomAssociation
{
CallingAETitle = callingAE,
CalledAETitle = calledAE,
Host = host,
Port = port,
ConnectTimeout = 10000,
AssociationTimeout = 30000
};
association.AddPresentationContext(
sopClassUID,
DicomTransferSyntax.ExplicitVRLittleEndian
);
association.Open();
DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_STORE_RQ);
request.AffectedSOPClassUID = sopClassUID;
request.AffectedSOPInstanceUID =
dataset[DicomTag.SOPInstanceUID]?.Value as string;
request.MessageID = 1;
DicomCommandSet response = association.SendRequest(request, dataset);
association.Close();
if (response.Status == 0)
{
Console.WriteLine("Send successful");
return true;
}
else
{
throw new DicomException($"Status: 0x{response.Status:X4}");
}
}
catch (Exception ex)
{
lastException = ex;
Console.WriteLine($"Attempt {attempt} failed: {ex.Message}");
if (attempt < maxRetries)
{
// Exponential backoff
int delay = retryDelayMs * attempt;
Console.WriteLine($"Waiting {delay}ms before retry...");
await Task.Delay(delay);
}
}
}
Console.WriteLine($"Max retries reached. Last error: {lastException?.Message}");
return false;
}
public async Task<List<string>> SendBatchWithRetry(
string callingAE, string calledAE, string host, int port,
List<string> filePaths)
{
var failedFiles = new List<string>();
int successCount = 0;
foreach (string filePath in filePaths)
{
Console.WriteLine($"Processing: {Path.GetFileName(filePath)}");
bool success = await SendWithRetry(callingAE, calledAE, host, port, filePath);
if (success)
successCount++;
else
failedFiles.Add(filePath);
}
Console.WriteLine($"Total: {filePaths.Count}, Success: {successCount}, " +
$"Failed: {failedFiles.Count}");
return failedFiles;
}
}
Best Practices Summary
DICOM Development Best Practices
================================
Connection Management:
✓ Use connection pooling for high-volume operations
✓ Implement proper timeout handling
✓ Close associations when done
✓ Handle network failures gracefully
Memory Management:
✓ Skip pixel data when only metadata is needed
✓ Use streaming for large files
✓ Implement proper disposal (using statements)
✓ Consider compression for storage
Error Handling:
✓ Implement retry logic with exponential backoff
✓ Log errors with sufficient detail
✓ Handle specific DICOM status codes
✓ Provide meaningful error messages
Performance:
✓ Use batch processing for multiple files
✓ Control parallelism to avoid resource exhaustion
✓ Cache frequently accessed data
✓ Monitor and optimize query performance
Security:
✓ Use TLS for network communications
✓ Validate AE Titles and IP addresses
✓ Implement proper authentication
✓ Log access and operations
Following these best practices ensures reliable, performant DICOM applications.