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 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.
![]()
![]()

