1998 Winter Quarter
Project 1 help session
This page is here to help you become familiar with the SGI Indy workstations
as quickly as possible. This page is also designed to assist you in completing
project 1, Impressionist. The following are quick tutorials for using subsets
of OpenGL, libui, CaseVision, as well as to help you understand the basics of
image processing -- enough to get you through project 1 -- and, for those of
you who have not created your own home page, there is a tutorial for creating a
very basic one as well.
Click here to download the lecture
slides from the help session.
Event driving programming is a programming style that is most commonly used
when interfacing to a graphical user interfaces (GUI) such as Windows95/NT
or X. Although it is a very common programming style, it isn't given
significant attention here, so this may be the first time that many of you
have seen it. Although event driven programming may seem intimidating at
first, it is not that bad once you get used to it. You just have to think
in a different way. In order to make your life easier (trust me on this
one...), we have come up with a library called libui that abstracts away
many of the headaches that come with the territory.
Libui is fully documented on the web.
Please see the documentation for any specific questions about libui
syntax. Please see the lecture slides if you
want a general overview of event driven programming and of how libui takes
advantage of the event driven programming model. If all else fails, send
us mail at cse457staff@cs.washington.edu
OpenGL - 2D/3D Graphics Library developed at Silicon Graphics Inc. OpenGL
has gained widespread industry acceptance in the high-end 3D markets, as well
as in the low-end consumer markets. OpenGL's main thrust is as a 3D programming
interface. Although it is targeted to the 3D market, it is more than adequate
for use in 2D. OpenGL provides fine grained control over images as well as 3D
objects. If you plan on continuing on in the computer graphics field after 457,
knowing OpenGL is a must.
Basic features:
- C language interface
- Is a state machine
- When you execute a command, it remains in effect until you specifically
change it.
- Graphic primitives:
- points, line segments, or polygons
- defined by a group of one or more vertices
- subject to several drawing modes which affect color, pattern,...
- Manipulation of frame buffer contents:
- useful for displaying images (arrays of data)
- also useful for handling expose events
Other features:
- Clipping, Shading, Texture mapping, etc.
Graphics Primitives
We will use OpenGL in the Impressionist program to draw the various brush
strokes. We will mainly stick to drawing lines and points. If you choose
to do so, you can make fancier brushes using some of the other primitives
(such as triangles, squares, and general polygons) for extra bells and
whistles. Here's a brief introduction (you'll find more in the man pages
and reference manuals)
- glColor - specify the color in which to draw primitive(s)
- glBegin, glEnd - delimit the vertices of a primitive or
a group of like primitives
- void glBegin( GLenum mode )
-
- GL_POINTS - treats each vertex as a single point
- GL_POLYGON - vertices define a solid, convex polygon
- see OpenGL manual for other modes
- glVertex - specify a vertex
- void glVertex2i( GLint x, GLint y )
- glFlush() - tells OpenGL to draw primitives now.
Example Code -- primitives:
The following bits of code show you how draw single-pixel dots,
a triangle outline (see OpenGL manual for other ways to do this), and a filled triangle
(as a three-sided polygon).
uiSetCurrentWindow( drawWind, 0 ); // libui call
glColor3f( red, green, blue );
// drawing a single-pixel dot to the
// window, at pixel coordinate (x,y)
glBegin( GL_POINTS );
glVertex2i( x, y );
glEnd();
// drawing an outlined triangle (many ways to
// do this) having vertices A, B, and C
glBegin( GL_LINE_STRIP );
glVertex2i( Ax, Ay );
glVertex2i( Bx, By );
glVertex2i( Cx, Cy );
glVertex2i( Ax, Ay );
glEnd();
// drawing a filled triangle having
// vertices A, B, and C
glBegin( GL_POLYGON );
glVertex2i( Ax, Ay );
glVertex2i( Bx, By );
glVertex2i( Cx, Cy );
glEnd();
glFlush(); // don't forget this!
Drawing images and other data
You could draw images one pixel at a time -- setting the color each time, telling
OpenGL to explicitly draw each pixel at each coordinate -- but this would be very
inefficient and doesn't always work anyway. That's why OpenGL provides the following
routines (and others).
The main routines:
- glReadBuffer, glDrawBuffer : used to specify the buffer that will be
read from or drawn to (e.g. GL_FRONT)
- glReadPixels, glDrawPixels : does the actual reading/writing
- void glReadPixels( GLint x,
GLint y,
GLsizei width,
GLsizei height,
GLenum format,
GLvoid type,
Glvoid *pixels )
-
- x, y - window coordinates of lower left corner of read source rectangle
- width, height - width and height of read source rectangle
- see OpenGL manual for details on other parameters
Other useful ones:
- glCopyPixels : copies a specified rectangular region of pixels from the ReadBuffer to the DrawBuffer relative to the current raster position
- glRasterPosxx : sets the raster position to start drawing, (0,0) is the lower left corner of an OpenGL window
- glPixelZoom : how much to scale the data when converting to pixels
Example Code: raster routines
// this routine will read the data from one window,
// and draw it on another
// allocates a chunk of memory to store enough data for 3 channels (r,g,b)
// of data for width*height pixels
unsigned char data[DRAW_HEIGHT][DRAW_WIDTH][3]
// read the data on one window
uiSetCurrentWindow( drawWind, uiNormal ); // libui call
glReadBuffer( GL_FRONT );
glReadPixels( 0, 0, DRAW_WIDTH, DRAW_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE,
&data[0][0][0] );
// now send it back out
uiSetCurrentWindow( otherWind, uiNormal ); // libui call
// set the right buffer and the right position, just in case
glDrawBuffer( GL_FRONT );
glRasterPos2i( 0, 0 );
glDrawPixels( DRAW_WIDTH, DRAW_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE,
&data[0][0][0] );
Notice that this example uses data stored as three bytes representing RGB (24-bit)
color. If you want to use indexed color, you will want to see the documentation
for glReadPixels and glDrawPixels for how to handle this.
Handling exposure events two schools:
- Redraw just redraw all the primitives to recreate the
window's contents -- useful for simple things like zoom boxes
- Backing store store all the data in your own structure
and redraw (like above).
In the Impressionist project, you will have to control various aspects of
the brush strokes, such as their position and orientation. Since you will
be using OpenGL to complete this project, you will find it very beneficial
to know a little bit about how "transformations" (shifting, rotation,
scaling, etc,) are done in OpenGL.
Recall that OpenGL is basically a state machine. For many aspects of it,
you setup certain parameters, and until you change them, GL will use those
parameters for everything it draws.
You have probably already seen how this is used for things like object color
(via glColor) and drawing mode (via glBegin / glEnd). However, there are
also state variables for things like position (accomplished via
"translation", or shifting) and direction (accomplished via rotation).
So how does this apply to Impressionist? Recall that you'll be drawing
various brush strokes on a digital canvas, each at a different position and
in a different direction. By setting the GL state variables for position
and orientation before drawing your brush, you can use the same code
regardless of the brush's position or orientation. The stroke will
automatically be drawn at the correct position and in the correct direction!
Here are the OpenGL calls needed to do some simple image transformations.
Pushing/Popping Matrices
Image transformations are accomplished using matrices. A series of matrices
are multiplied to produce a given image transformation. Without going into
too much detail, let's just say that you'll want to save your current
transformation matrix ("push" it onto a matrix stack) before doing your
brush-specific translation/rotation, and restore the original matrix ("pop"
it off the matrix stack) when you're done with that brush stroke.
Here are the calls you need to use.
glPushMatrix();
<<Do the translation, rotation>>
<<Draw the brush stroke>>
glPopMatrix();
Translation
To draw at a certain point, you'll want to "translate" your origin to that
position. Here is the call you use in OpenGL to do 2D translation:
glTranslate*( startX, startY, 0.0 );
(Note: the * is replaced by a letter that depends on the type of the parameters you pass.)
Rotation
You can also take advantage of OpenGL to automatically rotate the things you
draw. Here is the OpenGL call you'll want to use:
glRotate*( angle, 0.0, 0.0, 1.0 );
(Note: we use 0.0, 0.0, 1.0 because we want to rotate around the z-axis.)
Impressionist -- Important Project Files
To help you wade through all of the files we give you for Impressionist,
here is a brief list of the more important files and what they're used for:
- main.C - various user-interface-related items
- BrushControl.* - various user-interface-related items
- Brush.* - implementation of brush strokes
- XGradient.* - gradient handling
- YGradient.* - gradient handling
- SobelEdgeDetector.* - edge detection, for bell-and-whistle
You will most likely have to modify all of them.
To implement some of the requirements of this project, you will need
to know a little about image processing. More specifically, this document will
cover some of the aspects of edge detection and image gradients.
Some Basics
Often in image processing, we apply an operation to just a small
region of an image at a time. One of the most common techniques used
for applying such operations is by using something called a filter.
Diffenent image filters are distinguished by their kernel and their
support. Basically, you can think of a filter kernel as a grid
of values that we overlay on top of the pixels of an image. Here is
an example of a kernel:
|
0 |
1 |
1 |
1 |
0 |
|
1 |
1 |
2 |
1 |
1 |
|
1 |
2 |
3 |
2 |
1 |
|
1 |
1 |
2 |
1 |
1 |
|
0 |
1 |
1 |
1 |
0 |
We apply an operation to a pixel in the image as follows. First, imagine
placing the center of the kernel on top of the pixel of interest. Multiply
the value at each position in the kernel by the color of the pixel
underneath that position. Then, add up all these products, and divide by
the sum of the values in the kernel (if it's nonzero).
We can do many interesting things by changing the values in the kernel.
The rest of this section will explain how kernels and other image
manipulation methods are used to find edges and gradients in an image.
Image Blurring
The first step in finding edges and gradients in an image is to blur the
image. (Note: for finding edges and image gradients, we use a grayscale
version of the original image.) Blurring reduces the "noise" (randomness)
in the picture, and allows us to find edges and determine image gradients
more accurately.
There are several choices for the filter kernel to use for blurring
image. We will present three of them here.
- Box Filter: The first, and the simplest, is the "box filter". Basically, the kernel for the box filter is filled with all 1s.
- Bartlett Filter: Somewhat more complicated is the Bartlett filter. In this, pixels closer to the center are weighted more heavily than those further away. For the Bartlett filter, imagine placing a cone on top of the kernel, and using the height of the cone at each location to determine that position's relative weight.
- Gaussian Filter: Like the Bartlett filter, pixels near the center are weighted more heavily in the Gaussian filter. However, instead of a cone placed over the kernel, imagine a 3-dimensional bell curve on top of it.
In general, which filter you should use depends on the image you're
manipulating and what you want to accomplish. The box filter is the easiest
to implement, but the Bartlett and Gaussian filters often produce "better"
blurring results.
Edge Detection
Once you have blurred the grayscale version of the original image, you are
ready to do edge detection. The method of edge detection we present here is
known as Sobel edge detection.
To do Sobel edge detection, we once again use filters. This time we make
two passes over the image, using a different kernel each time. Here are
the filter kernels that are used.
By using both these filter kernels on each pixel in the image, we get
two new values at each pixel location (call them temp1 and temp2). We
then produce a single value at each location using this formula: sqrt(
temp1*temp1 + temp2*temp2 ). (You may notice a striking similarity to
the distance formula used in mathematics.) Finally, we say that if
this single value is above a certain threshold (usually specified by
the user), then there is an edge at that location.
Image Gradients
Given a blurred image, we can also produce the gradients for an image. To
intuitively understand what the image gradient is, recall what a gradient in
is in mathematics --- the direction of maximum rate of change. For a
surface (such as an array of pixels), the gradient is the direction in which
the color changes the fastest (and perpendicular to the gradient, the color
changes the slowest --- i.e., it's close to the same color).
Why is this important to us (and important for the Impressionist program, in
particular)? Well, if we know (at each pixel) the direction in which the
color is changing the least, we can orient our brush strokes in that
direction to make it more realistic. (Because if we paint a stroke in a
certain direction, the color of the paint stays the same during that
stroke!)
So how do we implement this? If you answer contained the word "filters,"
you're on the right track. We will actually produce two gradients, the
x-gradient (telling how fast the color changes along each row), and the
y-gradient (telling how fast the color changes along each column). Here are
the kernels that are used.
x-gradient filter kernel:
y-gradient filter kernel:
Using these kernels gives us two separate images, one of which tells us how
fast the color changes (at each pixel) in the x-direction, and one telling
us how fast it changes in the y-direction. Finally, we use this information
to determine the direction of the maximum rate of change in color, and we
orient our brush strokes perpendicular to that direction.
Debuggers on the Instructional SGIs
While printf is your friend as well as mine when it comes time to debug
things, you can save a lot of time by investing a small amount of time
to learn how to use a debugger.
We have a choice of debuggers on the SGIs in 228. We have a graphical
debugger called CaseVision, as well as the standard text based dbx
and gdb debuggers. I personally prefer dbx or gdb because there is not
as much overhead associated in using them and you are pretty much
guaranteed that they exist everywhere. Of course a text based, low
overhead program is much more difficult to use, so if it is simplicity
you're going after, CaseVision is probably the choice for you.
CaseVision is a very handy graphical debugging tool that will help you
greatly. Unfortunately (and quite ironically), it has a few bugs. These
are namely in how it handles killing processes, so be sure to check for
stray ones. Also, there are a few problems in debugging C++ code, so
beware because we will be using C++ all quarter. dbx on the SGIs handles
C++ well. I've never really used gdb, but I'm sure it handles C++ as well.
- Compile with the -g option (Makefile is already set appropriately)
prompt% cvd (executable_name)
- Load an executable (and core) choose the
Admin/Switch Executable menu option
- Load C/C++ source file to set breakpoints within - choose the
Source/Open menu option
- Set/remove breakpoints scroll through the source code, left-mouse-click
to select a breakpoint, and again to remove a breakpoint
- Control flow options:
- Step Over
- Step Into
- Return
- Continue
- Some Browsing options:
- Views/Variable browser
- Views/Call stack
- Views/Structure browser
- Views/Array browser
- Views/Expression browser
- Views/...
To use dbx, you will also have to compile with the -g option.
Once you have a core, type in:
prompt% dbx (executable_name) core
At the dbx prompt, you can type in t (which stands for trace) to get a stack
trace of the programs execution state when it core dumped.
You can also type in w (which stands for where) to see exactly at what line of
code the core dump occurred.
To navigate up and down through the stack frames, you can use the up and
down commands.
To print out variables, use p variableName.
There are many more commands that dbx supports. Type in help at dbx's command
line for a complete listing. You can also check out the dbx man page.
I've never really used gdb, so I'm not much help on how to use it. It's commands
are similar to dbx. As a matter of fact, you load a core file in the same way
that you do so in dbx.
To get a complete listing of the commands that gdb supports, type help at gdb's
command line. You can also check out the gdb man page.
- Take a picture of yourself
- Log into an Indy workstation, slide away the IndyCam's lens cover, and remove
the lens cap - yes, obvious
prompt% capture
- Select the still-camera icon from the menu button (lower left corner)
- If needed, adjust the settings (under Actions menu); you might want to
White Balance the IndyCam using the Video Panel (under Tools). You can
also focus the IndyCam (rotate the lens, like a normal camera).
- Center yourself in the red crop box
- Left-mouse-click on the record button to take a picture. Resulting
image is automatically saved as capture.rgb. To preview the image you
just made, hit the preview button.
- If you don't like the picture, take another one. Remember, though, that this
will write over your previous "capture.rgb". You might want to copy any
semi-decent ones to another filename, just in case you can't seem to get a better
one!
- Next, convert the image file to GIF format using
XView
% xv capture.rgb
This will load in your rgb file; to get the xv menu, click the
right-mouse button inside the image.
To save the file in the "gif" format, hit the Save button. You'll then
get a dialog box. Click the "gif" radio button, and change the name as
needed. Click Ok, and then Quit.
I'll leave any other exploration of xv (Color Editing, Cropping, etc.)
up to you.
- Doctor image as desired (scale, crop, etc.), then save image
in the GIF format with a .gif extension as in
your_name.gif
- Make a www sub-directory in your home directory on sanjuan or orcas,
and move your .gif image into the new directory.
You can reach your home directory (on sanjuan/orcas) from the SGI's with:
/homes/iws/yourname
You cannot, however, reach the SGI directories from sanjuan or orcas.
- For starters look at one of the TA's or a friends home page
(or click here for a dummy page), and
save the HTML source for that page by choosing the File/Save As
menu option and then selecting the HTML format. Load the file into a text
editor and modify it to your liking, and save it as index.html in your
www directory. There will be a line of HTML text like
< img src="tas_name.gif">
that you should change to
< img src="your_name.gif">
- Access
- To have your page appear in the CSE department pages,
create a file called .www-publish-me in your
home directory (on sanjuan or orcas).
- To allow access to your page to users outside the CSE net,
save
this file in your www directory (on sanjuan or orcas)
as ".htaccess"
- Browse the web to get ideas for your home page, and when you find
one you like, select the File/View Source menu option to see the HTML
source code will appear in the window. This is a great way to learn the features
of HTML -- well, the way I learned.
- For an official HTML primer,
click here
Send questions or comments to Mark Manca (mark@cs.washington.edu)