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);

I develop primarily on Linux and wrote class_dicom.php with Linux in mind. This tutorial will be geared towards CentOS or Fedora as that is what I use on a day to day basis. I’ll try to include notes for Ubuntu/Debian when possible.

I’m going to assume you have at minimum, Linux installed on a PC and you can access the command line as root.

1. Let’s get your environment ready

We’re going to install all of the needed development tools, a web server, and MySQL. A web server and MySQL is not needed by class_dicom.php, but since most folks are going to want to use PHP from a web server, we might as well get it installed.

CentOS:

yum -y groupinstall "Web Server" 'Development Tools' "MySQL Database"

yum -y install wget php php-gd ImageMagick libjpeg zlib libpng libtiff libxml2 libpng-devel zlib-devel libjpeg-devel libtiff-devel libxml-devel libxml2-devel libwrap-devel libpng10-develĀ  php-mysql

Ubuntu:

sudo apt-get install lamp-server^

sudo apt-get install build-essential wget ImageMagick libjpeg zlib libpng libtiff libxml2 libpng-devel zlib-devel libjpeg-devel libtiff-devel libxml-devel libxml2-devel libwrap-devel libpng10-devel

All of the above will probably take a while to finish out. Note that the ^ in the Ubuntu command is not a typo and needs to be present.

2. Build and install DCMTK

DCMTK is what class_dicom.php uses to actually work with DICOM files. Among many other things, it is a collect of command line tools for working with DICOM files.

The process of building and installing is essentially the same between versions of Linux.

wget ftp://dicom.offis.de/pub/dicom/offis/software/dcmtk/dcmtk360/dcmtk-3.6.0.tar.gz 
tar zxvf dcmtk-3.6.0.tar.gz
cd dcmtk-3.6.0
./configure;make;make install

Depending on your computer, the DCMTK can take a long time to build.

3. Install class_dicom.php

Download a copy:

wget --no-check-certificate http://github.com/vedicveko/class_dicom.php/zipball/master
unzip master
mv vedicveko-class_dicom.php* class_dicom_php

and…. you’re done.

Lets make sure it works!

cd class_dicom_php/examples
./get_tags.php dean.dcm

You should see a header dump of dean.dcm printed to the screen.

The script below will look in the directory you specify in $temp_dir and send any images it finds to a DICOM host. When it is done sending it will move the DICOM file to a back up directory.

#!/usr/bin/php
<!--?PHP 

require_once('class_dicom.php');

# WHERE YOUR DICOM FILES ARE
$temp_dir = '../temp';

# WHERE YOU ARE SENDING THEM
$target_host = 'some IP address';
$target_port = '105';
$target_ae = 'BK';
$my_ae = 'BK';

if(!file_exists('bk')) {
  mkdir('bk');
}

$d = new dicom_net;

if($handle = opendir($temp_dir)) {
  while(false !== ($file = readdir($handle))) {
    if($file != "." &#038;& $file != "..") {

      print "Sending $file...\n";

      $d--->file = "$temp_dir/$file";
      $ret = $d-&gt;send_dcm($target_host, $target_port, $my_ae, $target_ae);
      if($ret) {
        print "Send Error: $ret\n";
        continue;
      }
      else {
        print "Good Send\n";
        print "Moving $temp_dir/$file\n";
        rename("$temp_dir/$file", "bk/$file");
      }

    }
  }
  closedir($handle);
}

?&gt;