1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
|
#pydicom_Tkinter.py
#
# Copyright (c) 2009 Daniel Nanz
# This file is released under the pydicom (http://code.google.com/p/pydicom/)
# license, see the file license.txt available at
# (http://code.google.com/p/pydicom/)
#
# revision history:
# Dec-08-2009: version 0.1
#
# 0.1: tested with pydicom version 0.9.3, Python version 2.6.2 (32-bit)
# under Windows XP Professional 2002, and Mac OS X 10.5.5,
# using numpy 1.3.0 and a small random selection of MRI and
# CT images.
'''
View DICOM images from pydicom
requires numpy: http://numpy.scipy.org/
Usage:
------
>>> import dicom # pydicom
>>> import dicom.contrib.pydicom_Tkinter as pydicom_Tkinter # this module
>>> df = dicom.read_file(filename)
>>> pydicom_Tkinter.show_image(df)
'''
from __future__ import with_statement
import Tkinter
import tempfile
import os
have_numpy = True
try:
import numpy as np
except:
# will not work...
have_numpy = False
def get_PGM_bytedata_string(arr):
'''Given a 2D numpy array as input write gray-value image data in the PGM
format into a byte string and return it.
arr: single-byte unsigned int numpy array
note: Tkinter's PhotoImage object seems to accept only single-byte data
'''
if arr.dtype != np.uint8:
raise ValueError
if len(arr.shape) != 2:
raise ValueError
# array.shape is (#rows, #cols) tuple; PGM input needs this reversed
col_row_string = ' '.join(reversed(map(str, arr.shape)))
bytedata_string = '\n'.join(('P5',
col_row_string,
str(arr.max()),
arr.tostring()))
return bytedata_string
def get_PGM_from_numpy_arr(arr, window_center, window_width,
lut_min=0, lut_max=255):
'''real-valued numpy input -> PGM-image formatted byte string
arr: real-valued numpy array to display as grayscale image
window_center, window_width: to define max/min values to be mapped to the
lookup-table range. WC/WW scaling is done
according to DICOM-3 specifications.
lut_min, lut_max: min/max values of (PGM-) grayscale table: do not change
'''
if np.isreal(arr).sum() != arr.size:
raise ValueError
# currently only support 8-bit colors
if lut_max != 255:
raise ValueError
if arr.dtype != np.float64:
arr = arr.astype(np.float64)
# LUT-specific array scaling
# width >= 1 (DICOM standard)
window_width = max(1, window_width)
wc, ww = np.float64(window_center), np.float64(window_width)
lut_range = np.float64(lut_max) - lut_min
minval = wc - 0.5 - (ww - 1.0) / 2.0
maxval = wc - 0.5 + (ww - 1.0) / 2.0
min_mask = (minval >= arr)
to_scale = (arr > minval) & (arr < maxval)
max_mask = (arr >= maxval)
if min_mask.any(): arr[min_mask] = lut_min
if to_scale.any(): arr[to_scale] = ((arr[to_scale] - (wc - 0.5)) /
(ww - 1.0) + 0.5) * lut_range + lut_min
if max_mask.any(): arr[max_mask] = lut_max
# round to next integer values and convert to unsigned int
arr = np.rint(arr).astype(np.uint8)
# return PGM byte-data string
return get_PGM_bytedata_string(arr)
def get_tkinter_photoimage_from_pydicom_image(data):
'''
Wrap data.pixel_array in a Tkinter PhotoImage instance,
after conversion into a PGM grayscale image.
This will fail if the "numpy" module is not installed in the attempt of
creating the data.pixel_array.
data: object returned from pydicom.read_file()
side effect: may leave a temporary .pgm file on disk
'''
# get numpy array as representation of image data
arr = data.pixel_array.astype(np.float64)
# pixel_array seems to be the original, non-rescaled array.
# If present, window center and width refer to rescaled array
# -> do rescaling if possible.
if ('RescaleIntercept' in data) and ('RescaleSlope' in data):
intercept = data.RescaleIntercept # single value
slope = data.RescaleSlope #
arr = slope * arr + intercept
# get default window_center and window_width values
wc = (arr.max() + arr.min()) / 2.0
ww = arr.max() - arr.min() + 1.0
# overwrite with specific values from data, if available
if ('WindowCenter' in data) and ('WindowWidth' in data):
wc = data.WindowCenter
ww = data.WindowWidth
try:
wc = wc[0] # can be multiple values
except:
pass
try:
ww = ww[0]
except:
pass
# scale array to account for center, width and PGM grayscale range,
# and wrap into PGM formatted ((byte-) string
pgm = get_PGM_from_numpy_arr(arr, wc, ww)
# create a PhotoImage
# for as yet unidentified reasons the following fails for certain
# window center/width values:
# photo_image = Tkinter.PhotoImage(data=pgm, gamma=1.0)
# Error with Python 2.6.2 under Windows XP:
# (self.tk.call(('image', 'create', imgtype, name,) + options)
# _tkinter.TclError: truncated PPM data
# OsX: distorted images
# while all seems perfectly OK for other values of center/width or when
# the PGM is first written to a temporary file and read again
# write PGM file into temp dir
(os_id, abs_path) = tempfile.mkstemp(suffix='.pgm')
with open(abs_path, 'wb') as fd:
fd.write(pgm)
photo_image = Tkinter.PhotoImage(file=abs_path, gamma=1.0)
# close and remove temporary file on disk
# os.close is needed under windows for os.remove not to fail
try:
os.close(os_id)
os.remove(abs_path)
except:
pass # silently leave file on disk in temp-like directory
return photo_image
def show_image(data, block=True, master=None):
'''
Get minimal Tkinter GUI and display a pydicom data.pixel_array
data: object returned from pydicom.read_file()
block: if True run Tk mainloop() to show the image
master: use with block==False and an existing Tk widget as parent widget
side effects: may leave a temporary .pgm file on disk
'''
frame = Tkinter.Frame(master=master, background='#000')
if 'SeriesDescription' in data and 'InstanceNumber' in data:
title = ', '.join(('Ser: ' + data.SeriesDescription,
'Img: ' + str(data.InstanceNumber)))
else:
title = 'pydicom image'
frame.master.title(title)
photo_image = get_tkinter_photoimage_from_pydicom_image(data)
label = Tkinter.Label(frame, image=photo_image, background='#000')
# keep a reference to avoid disappearance upon garbage collection
label.photo_reference = photo_image
label.grid()
frame.grid()
if block==True:
frame.mainloop()
|