A Matter of Grayscale: Understanding Dicom Windows
DICOM images can contain a high amount of voxel values and windowing can be thought of as a means of manipulating these values in order to change the appearance of the image so particular structures are highlighted
DICOM images typically contain between 12–16 bits/pixel, which corresponds to approximately 4,096 to 65,536 shades of gray[1]. Most medical displays and regular computer screens are often limited to 8 bits or 256 shades of gray. There are high end medical displays that can display 1024 shades of gray (like the ones optimized for mammography).
However even with a computer screen that can display 256 shades of gray our eyes can typically only detect a 6% change in grayscale[2]
That means we can typically only detect about $100/6$ = 17 shades of gray
The [Hounsfield unit][3] (HU) scale is a quantitative scale for describing radiodensity. It applies a linear transformation where the radiodensity of distilled water at standard pressure and temperature (STP) is defined as 0 HU, while the radiodensity of air at STP is defined as -1000 HU.
(Image credit[4]) Most images will require viewing between -1000 HU (which is a reference for air) and +1000 HU (which typically references hard bone).
So a DICOM image can have a range of 2000 HU (from -1000 HU to +1000 HU) and if we wanted to display this range on a computer screen which can only display 256 shades of grey: $2000/256 = 8$. Then each shade of gray would have a difference of 8 HU.
The human eye can only detect a 6% change in grayscale so for humans to detect a difference in densities (within the image range of 2000 HU), each variation has to vary by: $256/17 * 8 = 120$ HU. The difference between normal and pathologically altered tissue is usually a lot less than 120 HU and this is why applying windows
is important.
Windowing can be thought of as a means of manipulating pixel values in order to change the apperance of an image so particular structures within the image are highlighted.
To futher explain how windowing
works we use this dataset, import the fastai
libraries and load a test image.
View the pixel_array
show_image(patient6.pixel_array, cmap=plt.cm.bone);
The difference when using fastai
is that by default it will display the normalized image when the show
function is used.
normalized = patient6.show(); normalized;
Clearly there is alot more depth displayed in the image. However this can be an issue when trying to localize areas that are normal to those that have been pathologically altered due to a condition. In most cases the difference in Hounsfield units between normal and pathologically altered tissue can be very small.
The normalized image displays a wide range of tissue densities(ranging from -1000HU (air) to around +1000HU (bone)). As mentioned above a regular computer screen can only display 256 shades of grey and our eye can only deduct about a 6% change in grayscale[2]
The basis of applying windows
is to focus down the 256 shades of grey into a narrow region of Hounsfled units that contain the relevant densities of tissues we may be interested in.
The fastai medical imaging library conveniently provides a number of window ranges that can be used by its windowed
function. The windows can be called by using dicom_windows
dicom_windows
A window has 2 values:
-
window level
orcenter
, also known as brightness,l
-
window width
orrange
, also known as contrast,w
Window pixel values are calculated using the following formulas:
-
lowest_visible_value
= window_level - window_width / 2 -
highest_visible_value
= window_level + window_width / 2
Using the brain window as an example: it has a window width
of 80 and a window level
of 40.
- lowest_visible_value = 40 - (80/2) = 0
- highest_visible_value = 40 + (80/2) = 80
In this case the lowest_visible_value
will be 0 and the highest_visible_value
of 80. This means that every pixel value greater than 80 will show up as white and any value below 0 will show up as black.
To see what this looks like at the pixel level we can scale down to a small portion of the image and see what effect windowing
has.
scaled = scaled_px(patient6)
crop = scaled[280:400,300:450]
show_image(crop, cmap='bone');
Scale down even further (picture above is to illustrate the area of the image we are looking at)
cropped = scaled[340:385,300:350]
show_image(cropped, cmap='bone');
We can now plot each pixel value
df = pd.DataFrame(cropped)
#uncomment below to view locally
#df.style.set_properties(**{'font-size':'8pt'}).background_gradient('bone').format("{:.1f}")
Using the lung window which has a window width
of 1500 and a window level
of -600.
- lowest_visible_value = -600 - (1500/2) = -1350
- highest_visible_value = -600 + (1500/2) = +150
In this case the lowest_visible_value will be -1350
and the highest_visible_value of +150
. This means that every pixel value greater than +150
will show up as white and any value below -1350
will show up as black.
windowed_image = cropped.windowed(*dicom_windows.lungs)
show_image(windowed_image, cmap=plt.cm.bone);
dfs = pd.DataFrame(windowed_image)
#uncomment below to view locally
#dfs.style.set_properties(**{'font-size':'6pt'}).background_gradient('bone').format("{:.1f}")
The pixel intensities have been scaled to values within the window range. To check that the window range is working as planned we can compare the pixel values of the original and the windowed image. Any pixel value above +150
will show up as white and any pixel value below -1350
will show up as black with various shades between those values.
#original image
df[1:2].style.set_properties(**{'font-size':'8pt'}).format("{:.1f}")
#windowed image
dfs[1:2].style.set_properties(**{'font-size':'8pt'}).format("{:.1f}")
Starting with the first pixel on the left, pixel value of 234
is higher than the highest_visible_value of +150
so this pixel will show up as white on the windowed image (represented by a 1.0
). The pixel value at column 21 is -731
which is not below the lowest_visible_value of -1350
so will not be shown as black but is represented by a shaded color(depending on the cmap setting) with a value of 0.4
.
Window Width
- Increasing the window width will
decrease
the contrast of the image - Decreasing the window width will
increase
the contrast of the image
high_width = patient6.windowed(w=1000, l=40)
low_width = patient6.windowed(w=100, l=40)
show_images([high_width, low_width], titles=['high window width', 'low window width'], figsize=(7,7))
Window Level
- Increasing the window level will
decrease
the brightness of the image - Decreasing the window level will
increase
the brightness of the image
high_level = patient6.windowed(w=200, l=200)
low_level = patient6.windowed(w=200, l=-200)
show_images([high_level, low_level], titles=['high window level', 'low window level'], figsize=(7,7))
There are numerous window ranges as specified in dicom_windows
or you could create your own by specifying the window width
and window center
. These are the most common window ranges used when particularly looking at images of the lungs.
- Window settings: (W:1600, L:-600)
- Usually used with a wide window to provide good resolution and to visualize a wide variety of densities.
lung = plt.imshow(patient6.windowed(*dicom_windows.lungs), cmap=plt.cm.bone); lung;
- Window settings: (W:500, L:50)
- The mediastinum is the area that separates the lungs. It is surrounded by the breastbone in front and the spine in back, with the lungs on either side. It encompasses the heart, aorta, esophagus, thymus (a gland in the back of the neck) and trachea (windpipe).
mediastinum = plt.imshow(patient6.windowed(*dicom_windows.mediastinum), cmap=plt.cm.bone); mediastinum;
- Window settings: (W:700, L:100) - this is not part of
dicom_windows
but you can easily use custom window ranges by specifying thewindow level
andwindow width
- Window range specifically used for looking at pulmonary embolisms
pe = plt.imshow(patient6.windowed(l=100, w=700), cmap=plt.cm.bone); pe;
Each window highlights particular ranges that make it easier for a radiologist to see if there are any changes between normal and pathologically altered tissue. If we compare the 3 images above with a non-windowed image we can clearly see why windowing is important.
show_images([patient6.hist_scaled(), patient6.windowed(*dicom_windows.lungs),patient6.windowed(*dicom_windows.mediastinum),\
patient6.windowed(l=100, w=700)], titles=['normalized', 'lung', 'mediastinum', 'pe'], figsize=(17,17));