Fanning Software Consulting

Image Contrast and Brightness Adjustment

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. 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: This article deals with a very specific case of image contrast adjustment, namely performing a linear contrast stretch interactively. If you are interested in more general image contrast enhancements, including linear, gamma, log, and inverse hyperbolic sine contrast adjustments, see the companion article, Improved Image Contrast.

It is probably easier to show you how interactive contrast stretching is done in medical images than to tell you how it is done. I've written an example program, named WindowImage, to demonstrate one way to accomplish this goal. (You will need programs from the Coyote Library to run this program.)

[Editor's Note: I've written a similar program, named ContrastZoom, using object 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 not totally enamored with the one I came up with, and I suspect you can probably do much better with a little thought. If you do, please send me a note. :-)

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.

   contrast = 50
   brightness = 50
   level = (1-brightness/100.)*(maxImage - minImage) + minImage
   width = (1-contrast/100.)*(maxImage - minImage)

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 WindowImage program, I start off with a contrast value of 50 and a brightness value of 50. You can change the brightness values 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. The color bar has been added so you can observe the windowing and level effect as you move the cursor. You can see what the program looks like in the figure below.

The ImageWindow program..
The Window/Level program as it appears on the display.
 

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

   displayMax = level + (width / 2)
   displayMin = level - (width / 2)
   cgImage, info.image, SCALE=1, MINVALUE=displayMin, MAXVALUE=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 info.maxImage THEN BEGIN
       difference = Abs(displayMax - info.maxImage)
       displayMax = displayMax - difference
       displayMin = displayMin - difference
   ENDIF
   IF displayMin LT info.minImage THEN BEGIN
       difference = Abs(info.minImage - displayMin)
       displayMin = displayMin + difference
       displayMax = displayMax + difference
  ENDIF

The tricky part of the algorithm comes when you have to decide how much movement of the cursor produces what kind of change in the brightness or contrast values. I decided to optimize my program for a "typical" image of about 512 by 512. So I divide the image size by 512 in the X direction and 2048 in the Y direction to create step factors in the algorithm. The step factors means that, in this window, you will have to move the cursor 1 pixel to achieve a change of one percent in contrast and about 4 pixels to achieve a change of one percent in brightness. These numbers were chosen empirically, so feel free to change them to suit your purposes.

The step factors (cstep and bstep, for "contrast step" and "brightness step", respectively) are stored in the info structure along with the initial X and Y location where the user sets the cursor down in the window. I save this location so I have a reference from which to judge how much to change the contrast or brightness. Here is my info structure definition:

    info = { image: image, $
             xsize: xsize, $
             ysize: ysize, $
             labelxsize: labelxsize, $
             labelysize: labelysize, $
             iWindow: iWindow, $
             iLevel: iLevel, $
             cbWinID: cbWinID, $
             imgWinID: imgWinID, $
             scale: scale, $
             x: -1, $
             y: -1, $
             imageAspect: imageAspect, $
             imgDrawID: imgDrawID, $
             cbDrawID: cbDrawID, $
             contrast: 50, $
             brightness: 50, $
             cstep: ysize / 512., $
             bstep: xsize / 2048., $
             minImage: minImage, $
             maxImage: maxImage }

The final step, then, is to write the event handler for the draw widget. I present it here without comment. Note that I don't allow 100 percent contrast, as this will allow the image to completely disappear.

   PRO WindowImage_DrawEvents, event

       ; Error handling.
       Catch, theError
       IF theError NE 0 THEN BEGIN
           Catch, /CANCEL
           void = cgErrorMsg()
           IF N_Elements(info) NE 0 THEN $
               Widget_Control, event.top, SET_UVALUE=info, /NO_COPY
           RETURN
       ENDIF
       
       ; What kind of event is this?
       possibleEvents = ['DOWN', 'UP', 'MOTION', 'VIEWPORT', 'EXPOSE']
       thisEvent = possibleEvents[event.type]
       
       CASE thisEvent OF
           
           'DOWN': BEGIN
               
               ; Get the program information.
               Widget_Control, event.top, GET_UVALUE=info, /NO_COPY
               
               ; Set the initial point.
               info.x = event.x
               info.y = event.y
               
               ; Clear events for this widget.
               Widget_Control, event.id, /CLEAR_EVENTS
               
               ; Turn motion events on.
               Widget_Control, event.ID, DRAW_MOTION_EVENTS=1
               
               ; Put the program information back in storage and return.
               Widget_Control, event.top, SET_UVALUE=info, /NO_COPY
               RETURN
               
               END
               
           'UP': BEGIN
           
               ; Get the program information.
               Widget_Control, event.top, GET_UVALUE=info, /NO_COPY

               ; Turn motion events off and clear any queued up events.
               Widget_Control, event.ID, DRAW_MOTION_EVENTS=0
               Widget_Control, event.id, /CLEAR_EVENTS
               
               ; Calculate new contrast/brightness values.
               contrast = 0 > ((info.y - event.y) * info.cstep + info.contrast) < 99
               brightness = 0 > ((info.x - event.x) * info.bstep + info.brightness) < 100
               level = (1-brightness/100.)*(info.maxImage - info.minImage) + info.minImage
               width = (1-contrast/100.)*(info.maxImage - info.minImage)
               
               ; Calculate new display min/max.
               displayMax = (level + (width / 2))
               displayMin = (level - (width / 2))
               IF displayMax GT info.maxImage THEN BEGIN
                  difference = Abs(displayMax - info.maxImage)
                  displayMax = displayMax - difference
                  displayMin = displayMin - difference
               ENDIF
               IF displayMin LT info.minImage THEN BEGIN
                  difference = Abs(info.minImage - displayMin)
                  displayMin = displayMin + difference
                  displayMax = displayMax + difference
               ENDIF
               
               ; Store everything.
               info.iWindow = [displayMin, displayMax]
               info.iLevel = level
               info.contrast = contrast
               info.brightness = brightness
               info.x = event.x
               info.y = event.y
               
               ; Display the undated image.
               WindowImage_Display, info
               
               ; Put the program information back in storage and return.
               Widget_Control, event.top, SET_UVALUE=info, /NO_COPY
               
               END
               
           'MOTION': BEGIN

               ; Get the program information.
               Widget_Control, event.top, GET_UVALUE=info, /NO_COPY
                
               ; Calculate new contrast/brightness values.
               contrast = 0 > ((info.y - event.y) * info.cstep + info.contrast) < 99
               brightness = 0 > ((info.x - event.x) * info.bstep + info.brightness) < 100
               level = (1-brightness/100.)*(info.maxImage - info.minImage) + info.minImage
               width = (1-contrast/100.)*(info.maxImage - info.minImage)
               
               ; Calculate new display min/max.
               displayMax = (level + (width / 2))
               displayMin = (level - (width / 2))
               IF displayMax GT info.maxImage THEN BEGIN
                  difference = Abs(displayMax - info.maxImage)
                  displayMax = displayMax - difference
                  displayMin = displayMin - difference
               ENDIF
               IF displayMin LT info.minImage THEN BEGIN
                  difference = Abs(info.minImage - displayMin)
                  displayMin = displayMin + difference
                  displayMax = displayMax + difference
               ENDIF
    
               ; Store the information.           
               info.iWindow = [displayMin, displayMax]
               info.iLevel = level
               
               ; Update the image.            
               WindowImage_Display, info
               
               ; Put the program information back in storage and return.
               Widget_Control, event.top, SET_UVALUE=info, /NO_COPY
               
               END
       
       ENDCASE
       
       
   END ;---------------------------------------------------------------------------------------

The image display routine, WindowImage_Display, is shown here.

   PRO WindowImage_Display, info

       SetDecomposedState, 1, CURRENTSTATE=currentState

       WSet, info.imgWinID
       cgImage, info.image, SCALE=info.scale, NCOLORS=253, /KEEP_ASPECT, $
           MINVALUE=info.iwindow[0], MAXVALUE=info.iwindow[1]

       WSet, info.cbWinID
       cgErase
       cgColorbar, NCOLORS=253, CLAMP=info.iwindow, NEUTRALINDEX=254, $
           POSITION=[0.05, 0.35, 0.95, 0.65], FONT=0, ANNOTATECOLOR='black', $
           RANGE=[info.minImage, info.maxImage], DIVISIONS=5, FORMAT='(F0.2)'
       
       format = '(F0.3)'
       txt = 'Window: [' + String(info.iwindow[0], FORMAT=format) + ', ' + $
                           String(info.iwindow[1], FORMAT=format) + ']  Level: ' + $
                           String(info.ilevel, FORMAT=format)
       cgText, 0.5, 0.75, /Normal, Alignment=0.5, Font=0, txt, Color='black'
     
       SetDecomposedState, currentState

   END 

Version of IDL used to prepare this article: IDL 7.0.1.

Last Updated: 24 March 2011