Fanning Software Consulting

Interactively Setting the Window/Level of an Image

QUESTION: I have a 16-bit medical image. I would like to interactively drag the cursor to adjust the contrast and brightness of this image using object graphics. I understand that this is also sometimes called adjusting the window level and width of the image. Can you show me how this is done?

ANSWER: It is probably easier to show you how it is done than to tell you how it is done. I've written an example program, named ContrastZoom, to demonstrate one way to accomplish this goal. In addition to demonstrating how to window/level an image, I also show how to zoom an image "in place".

[Editor's Note: I've written a similar program, named WindowImage, using direct graphics. You can read about that program in this article.]

The tricky part of this program is coming up with an algorithm that smoothly changes the contrast and brightness in the image as you move the cursor over the image. I'm indebted to Sean La Shell of Massachussets General Hospital for providing the general algorithm used in this program in an IDL newsgroup article on the subject. He may not recognize it after the going over I have given it, but it was extremely useful to get me started in the right direction.

The basic idea is this. Assume that contrast and brightness are values that can vary from 0 to 100. Given that you know the minimum and maximum values of the image, you can find the level and width of the "window" into this image like this:

   minval = Min(image)
   maxval = Max(image)
   level = (1-brightness/100.)*(maxVal - minVal) + minVal
   width = (1-contrast/100.)*(maxVal - minVal)

In this sense, level means the image value at the center of the window, and width defines the size of the window of image values. You can think of a window or box that slides up and down a number line representing image values. The window can be bigger (encompassing more image values) or smaller (encompassing fewer image values). The center point of the window on the number line is the window level.

In the ContrastZoom program, I start off with a contrast value of 25 and a brightness value of 75. You can change the brightness values in the center image by dragging the cursor on the image in a horizontal direction. You can change the contrast values by dragging the cursor in a vertical direction. Of course, you can simultaneously change both brightness and contrast by moving the cursor on any diagonal direction. Two color bars have been added so you can observe the windowing and level effect as you move the cursor. The second color bar (farthest right) is a subset of the first, and shows you the extent of the displayMin and displayMax variables, which are discussed below. The image at the left in the program can be zoomed into by dragging a rubberband box around the area of the image you wish to see more clearly. You can restore the starting values in either image by simply clicking and releasing the cursor inside the image window. You can see what the program looks like in the figure below.

The ContrastZoom program.

Given that you can calculate a level and width, how then do you display the image? You do it my calculating the minimum and maximum values to use in the BytScl command, like this:

   displayMax = level + (width / 2)
   displayMin = level - (width / 2)
   scaledImage = BytScl(image, Min=displayMin, Max=displayMax)

After playing with this algorithm for awhile, I realized that the window could be outside the data range if the level gets too high or too low. Thus, I modified the algoithm to keep the window always within the data range, like this.

   displayMax = level + (width / 2)
   displayMin = level - (width / 2)
   IF displayMax GT maxval THEN BEGIN
      difference = Abs(displayMax - maxVal)
      displayMax = displayMax - difference
      displayMin = displayMin - difference
   IF displayMin LT minval THEN BEGIN
      difference = Abs(minVal - displayMin)
      displayMin = displayMin + difference
      displayMax = displayMax + difference
   scaledImage = BytScl(image, Min=displayMin, Max=displayMax)

In an object graphics program, the image is updated by changing the data of the image object, like this:

   contrastImage->SetProperty, Data=scaledImage

You can learn the details by reading the program code and notes.

Zooming "In Place" in Object Graphics Programs

A second technique I wanted to demonstrate with this program is how to zoom an image "in place" in a way that preserves the aspect ratio of the image subset. That is to say, rather than "zooming" into the image by replicating pixels, I want to zoom into a image by getting as "close to" the image subset as I can. So I make the image subset "fit" into the window I have allocated for the image in such a way that the image subset is as large as possible and preserves its aspect ratio (ratio of height to width).

To do this, I set up a coordinate system in my viewport rectangle that goes from 0 to 1 in both the X and Y directions. Then I modified the Aspect program from the Coyote Library for my purposes here. Given the aspect ratio of an image, and an aspect ratio of a window, the Aspect program will calculate the largest position in the window that will preserve the image's aspect ratio. Typically, this program is used to produce a plot in the window with a particular aspect ratio. (I have renamed the Aspect program as ContrastZoom_Aspect in the program code.)

The code, then, to locate an image object in a particular view or window on the display, looks like this. (The Normalize program translates and scales the range to the correct values.)

   s = Size(imageSubset, /Dimensions)
   info.zoomImage->SetProperty, Data=BytScl(imageSubset, Min=info.minval > $
      displayMin, Max=displayMax < info.maxval), Dimensions=s
   imageAspect = Float(s[1]) / s[0]
   info.theWindow->GetProperty, Dimensions=dims
   windowAspect = (450./info.window_ysize * dims[1]) / (300./info.window_xsize * dims[0])
   pos = ContrastZoom_Aspect(imageAspect, WindowAspect=windowAspect, Margin=0)
   info.zoomImage->GetProperty, XRange=xrange, YRange=yrange
   xs = Normalize(xrange, Position=[pos[0], pos[2]])
   ys = Normalize(yrange, Position=[pos[1], pos[3]])
   info.zoomImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys

You can find the rest of the details in the program code and notes.

Web Coyote's Guide to IDL Programming