Fanning Software Consulting

Drawing a Rubberband Box

QUESTION: How do I draw a rubberband box on an image in a regular graphics window?

ANSWER: If you look at a routine like Box_Cursor you will see that the rubberband box is drawn with a technique that uses the XOR (eXclusive OR) graphics function. In this technique, the rubberband box is drawn and erased by using the XOR graphics function of the window. As the box is drawn in the window, the underlying pixels are essentially "flipped" to their opposite color. Drawing the box a second time "flips" the pixels to their original color, thereby erasing the box.

I don't care much for this method because it doesn't allow me to specify the color I draw the box in. The box is drawn in the "opposite" pixel color and this can vary from pixel to pixel, resulting in a box that sometimes has a mottled look. I prefer to draw a yellow or green box almost always. Thus, I prefer to use a technique that I call the "Device Copy" technique. In this technique a pixmap (an IDL graphics window that exists only in memory) is used to erase the box after it has been drawn.

The method is outlined like this. Click in the window. This is the static (unchanging) corner of the box. Now go into a loop. The first thing you do in the loop is erase the previous box (using the Device Copy technique) and immediately get the next cursor location. This is the dynamic (changing) corner of the box. Draw the box. Do this over and over again until the user indicates they want out of the loop. (There are various ways to do this, but I'm going to implement it so that I will get out of the loop when the user releases the mouse button. In other words, to draw the box the user has to click and drag the mouse in the window.)

In the Device Copy method, the Copy keyword is passed a seven-element array. You always Copy a rectangular portion of the source window into the current graphics window. (The current graphics window is also called the destination window.) In this case, the source window is the pixmap window, which is set up to look identical to the display window, except that it doesn't have a box drawn on it. The Device command will look like this:

   Device, Copy=[sx, sy, columns, rows, dx, dy, sourceID]

where, sx and sy are the coordinates of the lower-left corner of the copy rectangle in the source window, columns and rows are the number of columns and rows in the copy rectangle, dx and dy are the coordinates of the lower-left hand corner of the copy rectangle in the destination window (i.e., they tell you where to place the rectangle in the destination window), and sourceID is the window index number of the source window.

In practice, the Device command will be written like this:

   Device, Copy=[0, 0, xsize, ysize, 0, 0, pixID]

where xsize and ysize are the sizes of the display and pixmap windows, and pixID is the window index number of the pixmap.

Here is a program, named DrawBox, that allows you to draw a rubberband box in any window. It accepts as a parameter the window index number of the window you wish to draw in. If you don't provide a parameter, it uses the current window as the display window.

The program is written as a function and the coordinates of the final box are returned as a four-element array. This allows you to do something with the information. Maybe you are zooming into a plot or image, or subsetting the data, etc. Keywords allow you to have Normalized or Data coordinates returned rather than the default Device coordinates. You can also specify a color for the box with the Color keyword.

The code looks like this:

Function DrawBox, $
   wid, $         ; ID of window where box is drawn. (!D.Window by default.)
   Color=color, $ ; The color index of the box. (!D.N_Colors-1 by default.)
   Data=data, $   ; Box coordinates returned as DATA coordinates.
   Normal=normal  ; Box coordinates returned as NORMAL coordinates.

; This function draws a rubberband box in the window specified
; by the positional parameter (or the current graphics window, by
; default). The coordinates of the final box are returned by the
; function. Click in the graphics window and drag to draw the box.

   ; Catch possible errors here.
   
Catch, error
IF error NE 0 THEN BEGIN
   ok = Widget_Message(!Err_String)
   RETURN, [0,0,1,1]
ENDIF

   ; Check for parameters.

IF N_Params() EQ 0 THEN wid = !D.Window > 0
IF N_Elements(color) EQ 0 THEN color = (!D.N_Colors - 1) < 255

   ; Make current window active.

WSet, wid
xsize = !D.X_VSize
ysize = !D.Y_VSize

   ; Create a pixmap for erasing the box. Copy window
   ; contents into it.

Window, /Pixmap, /Free, XSize=xsize, YSize=ysize
pixID = !D.Window
Device, Copy=[0, 0, xsize, ysize, 0, 0, wid]

   ; Get the first location in the window. This is the
   ; static corner of the box.

WSet, wid
Cursor, sx, sy, /Down, /Device

   ; Go into a loop. Stay in loop until button is released.

REPEAT BEGIN

      ; Get the new cursor location (dynamic corner of box).

   Cursor, dx, dy, /Change, /Device

      ; Erase the old box.

   Device, Copy=[0, 0, xsize, ysize, 0, 0, pixID]

      ; Draw the new box.

   PlotS, [sx, sx, dx, dx, sx], [sy, dy, dy, sy, sy], $
      /Device, Color=color

ENDREP UNTIL !Mouse.Button EQ 0

   ; Erase the final box.

Device, Copy=[ 0, 0, xsize, ysize, 0, 0, pixID]

   ; Delete the pixmap.

WDelete, pixID

   ; Order the box coordinates and return.

sx = Min([sx,dx], Max=dx)
sy = Min([sy,dy], Max=dy)

   ; Need coordinates in another coordinate system?

IF Keyword_Set(data) THEN BEGIN
   coords =  Convert_Coord([sx, dx], [sy, dy], /Device, /To_Data)
   RETURN, [coords[0,0], coords[1,0], coords[0,1], coords[1,1]]
ENDIF

IF Keyword_Set(normal) THEN BEGIN
   coords =  Convert_Coord([sx, dx], [sy, dy], /Device, /To_Normal)
   RETURN, [coords[0,0], coords[1,0], coords[0,1], coords[1,1]]
ENDIF

   ; Return device coordinates, otherwise.

RETURN, [sx, sy, dx, dy]
END

To use this program on an 8-bit display, type these commands:

file = Filepath(SubDirectory=['examples','data'], 'ctscan.dat')
OpenR, lun, file, /Get_Lun
image = BytArr(256, 256)
ReadU, lun, image
Free_Lun, lun

TvLct, 255, 255, 0, (!D.N_Colors-1)  ; Yellow color
Window, 5, XSize=256, YSize=256
TV, BytScl(image, Top=!D.N_Colors-2)
box = DrawBox(5, Color=!D.N_Colors-1)
subimage = image[box[0]:box[2], box[1]:box[3]]
Print, 'Average value of pixels inside final box: ', $
   Total(subimage) / N_Elements(subimage)

Drawing rubberband boxes in a widget program is slightly different, because you don't want to be writing a loop in a widget program. You want to take advantage of the fact that the widget program is itself a loop. To see how this is done, look at the programming tip on drawing a rubberband box in a draw widget window.

[Return to IDL Programming Tips]