DICOM in Python?

I do most of my software development these days using Python under Windows. I had the idea of rewriting my DICOM PHP class in Python with the mind of ease of use under Windows and including any needed DCMTK binaries in a nice package. From past experience with other languages I knew it could be difficult to start other programs in the background on Windows and then keep track of them. I set out to write a simple store and forward application using Python and the DCMTK to see what I was getting myself into.

Turns out Python has some really nice modules for running programs in the background. In a very short period of time I had the beginnings of pretty slick DICOM application. Even as a quick and dirty test application it is able to:

  • Read DICOM tags from files.
  • Start promiscuous DICOM storage services on port 4343
  • Write any images it receives into a directory.
  • Watch that directory for files and detect if those files are DICOM.
  • Compress DICOM files, using a compress type suitable to the file’s modality
  • Send DICOM files to a remote host
  • Perform a DICOM ping in order to tell if a DICOM host is up

You can download all of my files here. Just extract the zip, the good stuff is in snf.py. Take note that this is just proof of an idea and by no means safe or production worthy code.

If you don’t feel like downloading it, here is the complete source:

import subprocess
import time
import os  
import xml.etree.ElementTree as ET

# Is the file a DICOM file?
def is_dcm(file):
  f = os.popen('dcmtk/bin/dcm2xml.exe ' + file)
  out = f.read()
  if not "TransferSyntaxUID" in out:
    return False
  return True

def echoscu(host, port):
  f = os.popen('dcmtk/bin/echoscu.exe ' + host + ' ' + port)
  out = f.read()
  if not out:
    return True
  return False

# Dump out the DICOM header into an array indexed by the tag tags['0010,0010'] = some value
def dcm_dump(file):
  # Run dcm2xml to get some XML of the DICOM headers
  f = os.popen('dcmtk/bin/dcm2xml.exe ' + file)
  out = f.read()
  root = ET.fromstring(out)

  # The XML we get doesn't make it easy to look up tags. Lets reorganize it
  tags = {}

  # Get the meta tags  
  for child in root:
    tag = child.tag
    value = child.attrib['name']
    name = child.attrib['name']
    tags[tag] = value 

  # Get the real tags
  for child in root.iter('element'):
    tag = child.attrib['tag']
    value = child.text
    name = child.attrib['name']
    tags[tag] = value

  return tags

# pull the value from the array generated by dcm_dump()
def get_tag(tags, tag):
  try:
    tags[tag]
  except:
    return ''
  
  val = tags[tag]
  return val

def storescu_switch(ts):
  if 'JPEG Baseline' in ts:
    return '-xy'
  elif 'JPEG Extended' in ts:
    return '-xx'
  elif 'JPEG Lossless' in ts:
    return '-xs';
  else:
    return ''

  

# Compress a DICOM file if need be
def compress_dcm(modality, ts, file):
  switch = ''
  new_ts = ''
  if not 'JPEG' in ts: # we need to compress
    tmp_file = 'temp.dcm'
    
    if modality == 'US':
      switch = '+eb'
      new_ts = 'JPEG Baseline'
    elif modality == 'CR' or modality == 'DR' or modality == 'DX' or modality == 'SC' or modality == 'RG' or modality == 'OT':
      swtich = '+ee'
      ts = 'JPEG Extended'
    else:
      switch = ''
      new_ts = 'JPEG Lossless'

    f = os.popen('dcmtk/bin/dcmcjpeg.exe ' + switch + ' ' + file + ' ' + tmp_file)
    out = f.read()
    if(out):
      print out
      new_ts = ''
    else:
      os.remove(file)
      os.rename(tmp_file, file)
    
  if(new_ts):
    ts = new_ts
  return ts

# Send a DICOM file, compress if needed
def send_dcm(file):
  print file
  tags = dcm_dump(file)
  
  ts = compress_dcm(get_tag(tags, '0008,0060'), get_tag(tags, 'data-set'), file)
  
  switch = storescu_switch(ts)
  my_ae = 'PYTHON'
  remote_ae = 'DEANO'
  host = '192.168.1.216'
  port = '105'

  print ts
  print switch

  f = os.popen('dcmtk/bin/storescu.exe -ta 10 -td 10 -to 10 ' + switch + ' -aet ' + my_ae + ' -aec ' + remote_ae + ' ' + host + ' ' + port + ' ' + file)
  out = f.read()
  
  print out
  if not out:
    os.remove(file)
    print file + " sent OK"
  
# Program flow
# Start storescp
# Loop through temp_images directory
# Detect dicom file
# Figure out if compressed
# If not, compress based on modality
# Send File in another thread
# Send OK: Delete File
# Check on storescp... echoscu myself... if no good for five tries... kill and restart storescp

# Directories and defines
base_dir = "C:\\rrad\\r"
temp_images = base_dir + '\\temp_images'

listen_port = '4343' # Hopefully always free

# Start storescp
storescp_args = "-dhl -td 20 -ta 20 -xf " + base_dir + "\\storescp.cfg Default -od " + temp_images + ' ' + listen_port
subprocess.Popen("dcmtk/bin/storescp.exe " + storescp_args)


x = 0 # count runs
bad_echoscus = 0 # Count failed echoscus

# Main loop
while True:
#  print temp_images + " : " + storescp_args + "\n"
  print "Run: ", x

  # check out the temp_images dir for DICOM files
  for fn in os.listdir(temp_images):
    if is_dcm(temp_images + '\\' + fn):
      send_dcm(temp_images + '\\' + fn) # Found one, send it
    else:
      os.remove(temp_images + '\\' + fn)
      print "Removed non-DICOM file: " + fn

  # Every 5 runs, echoscu myself   
  if x == 5:
    if echoscu('localhost', '4343'):
      print "ECHOSCU OK"
      bad_echoscus = 0
    else:
      print 'Echoscu NG'
      bad_echoscus += 1

    x = 0
    if bad_echoscus == 3:
      print "Too many bad echoscus"

  x += 1 
  time.sleep(5);

5 comments

  1. Hi,

    Great post! I was wondering how would you handle multi-frame files (ultrasound, or “cine”) files such that it would be possible to write it to an avi file?

      1. Thanks Dean for the reply.

        I’m wondering what’s a reliable way to determine whether a dcm is multi-frame? From what I read, it should be based on SOP class. But I’m not sure how to code for it?

        I noticed in your code you do:

        $multi = $d->get_tag(‘0002’, ‘0002’); // Is this a multi-frame video

        if(strstr($multi, ‘Multi’))

        So you’re looking for the substring ‘Multi’ in that particular tag, but the sample.dcm has this for that tag:

        1.2.840.10008.5.1.4.1.1.3.1

        How can the substring test be working?

Leave a Reply to Randi Cancel reply

Your email address will not be published. Required fields are marked *