Goals for this Pre-lab

  • Learn about Image representations
  • Set up your PNG environment
  • Get your files ready for lab

Remember to create a new directory in your repository for this lab. (Refer to previous labs for how if you have forgotten)

You have two tasks to complete before your lab. First, read about images, #define, and 2d arrays and answer the questions in lab3questions.html.

Second, create the header file and skeleton implementation file for the functions you will implement in the warmup during lab. More detailed directions follow.

Step 1: Image files

I have intentionally not provided directions on how to create and add your lab3_questions.html file. If you have forgotten, look at a previous prelab for directions.

For the warmup and homework, you will be using png, a standard image format for storing pictures (png stands for "portable network graphics"). The png format exists independent of C programming; it is a complex compressed image format that you can produce (or consume) with programs written in any language. It is fairly efficient representation, and compresses images without any loss in quality (also known as lossless compression). This fact will be very useful in our upcoming assignment. Learning the PNG format is beyond the scope of this assignment; we will provide the functions that enable you to read and write PNG files. However, you still have to understand how images can be represented digitally.

An image can be represented as an array of pixels. Each pixel has an associated color. Colors in programs are stored as a series of three values: rgb - red, green, and blue. In our png format, we will allow the values to range from no color (0) to full saturated (255). Any color can be created using these three numbers. As you would expected, red is (255,0,0), green is (0,255,0), and blue is (0,0,255). When combining colors, though, you need to remember that these are light palettes, not paint palettes. Therefore, white is the saturation of all colors of light (255,255,255), and black is the absence of light (0,0,0). To get the values for red, orange, yellow, green, blue, and purple, you can look at this website.

In this assignment, you will be working with arrays of these red, green and blue values. We will provide functions that can read and write PNG files. These functions are defined in hw3_provided.h and hw3_provided.c), namely the provided_read_png and provided_write_png functions. For this assignment, you can simply invoke these functions in your code, and do not need to understand how they work. In order to compile your code, you will need a library called libpng installed on your system. If you are having trouble setting up the environment, it is suggested that you use the linux.cs.uchicago.edu machines, which already have this library installed.

If you are running MacOS which has Homebrew installed you can probably run:

brew install libpng

On Linux or Bash for Windows you can run this:

sudo apt install libpng-dev

If you are running Cygwin, it might be a bit harder, but i suggest installing apt-cyg and running:

apt-cyg install libpng-devel

To compile the provided functions, you can simply call:

	clang -c -Wall -o hw3_provided.o hw3_provided.c

To link this function correctly using the png library with your own solutions, we add the -lpng flag to clang:

	clang -Wall -lpng -o hw3 hw3_provided.o warmup3.c warmup2_main.c

These operations should be added to your Makefile.

The functions are designed to only read certain types of PNG files for this assignment. For those who are interested in the details, the files must be:

  • Limited to 50 pixels in height and width
  • Use the RBG color space without any transparency

Here are a set of PNG files that have been tested to work with these functions. You may use them for your warmup and hw3. If you would like to use your own images, you may do so by editing them in an image editor (Photoshop, GIMP, etc.) to comply with the specifications above, at your own peril!

Step 2: #define

We have seen #define now a few times. There are two aspects to it: Exactly how it works and the proper use in C.

#define (and other #commands) are commands for the C pre-processor. This is the first pass that happens in C compilation. The pre-processor goes through the set of compilation files looking for those commands. Right now, we will focus on the #define command.

#define HOWDY 5

This command tells the pre-processor to go through all of the files (after it reads #define) and replace every string "HOWDY" with the number 5. The reason we use all caps is that we don't want to accidentally have "HOWDY" in something else. For example, if we did the following:

#define a 5
If we had the following code:
char my_function(char x, char y)

The pre-processor would replace all fo the 'a''s with 5. This would break all of our code. Therefore, pre-processor commands are both powerful and very, very stupid. They are only to be used in particular circumstances.

Use 1: Constants. The use above was as a constant. If you want to define a constant that will be used throughout the file, this is a great use of #define. int scores[MAX_STUDENTS] is a great use of a constant. We might use that several times in the file. If we use a pre-processor command, then we can define it once at the top of the file. If the number of students ever changes, we change it in only one location. The advantage of using #define over a variable is that the compiler is able to optimize constants better than variables - it doesn't need to read in the value.

Use 2: Defining conditions. We see this in the .h file. We see

 #ifndef HW2_H
#define HW2_H
... rest of file
#endif

In this case, we're not define HW2_H to be a value - we're just defining it. This is only useful for #ifndef through #endif. This is a special if statement for the preprocessor. In this case, we are using the if statement to make sure the same code is not compiled twice.

Use 3: debug code. It is common for programmers to insert many print statements when debugging the code. When they fix the bug, then they want to remove the prints, but if they find another bug, it is useful to put them back in again. The quickest way to do this is to put your print statements inside #ifdef statements like this:

#ifdef DEBUG
printf("Line 74: scores[%d] has value %f\n",i,scores[i]);
#endif
Then, if you want the debugging statements to print out, you put #define DEBUG at the top of the file. To remove the print statements, remove the #define line.

In this assignment, we are going to add some #define constants to the .h file. The purpose is so that our testing infrastructure can change those constants in order to test that your code has not been hard-coded to particular values. This is used in exercises 3 and 4.

Step 3: 2-D arrays

Problems 2 and 3 use 2-d arrays to store the information necessary for an image. To pass a 1-d array to a function, you don't need to tell the compiler its dimensions. For a 2-d array, however, the compiler needs to know the dimensions in order to produce correct code. We are going to use #define to define the array sizes. Then we can use it when passing 2-d arrays.

#define ROWS 50
#define COLS 10
int row_max(int array[ROWS][COLS], int row);
int col_max(int array[ROWS][COLS], int col);
float row_avg(int array[ROWS][COLS], int row);
float col_avg(int array[ROWS][COLS], int col);

To declare a 2-d array, you do the following:
int my_array[50][100];
or
int my_array[ROWS][COLS];

To declare and initialize a 2-d array in a single command, you can use this syntax:
int a[3][4] = {
   {0, 1, 2, 3} ,   /*  initializers for row indexed by 0 */
   {4, 5, 6, 7} ,   /*  initializers for row indexed by 1 */
   {8, 9, 10, 11}   /*  initializers for row indexed by 2 */
};

There are two ways that arrays are different when passing to and receiving as return values from arrays. You have been introduced to pointers. Arrays are passed like pointers - only the address of the beginning element is passed; the entire array is not copied. This has two implications.

First, they can be out parameters. That is, if you make any changes to the array within the function, those changes were made to the original array, so those changes will "stick" after the function call.

Second, when declared statically, as seen above, they are local variables. Local variables are destroyed once a function call ends. Therefore, they may not be the return value of a function. We will learn how to make an array that you want to return in a week or so.

Step 4: Skeleton files: main, .c, .h

During the warmup, you are going to implement several functions that exercise arrays and pointers.

In order to spend your time efficiently in lab, you need to start with a skeleton that compiles. Create these, fill them in, and verify that your project is compiling. You will need warmup3_main.c, warmup3.h, and warmup3.c. For your homework, you will also need some functions I am providing: hw3_provided.h and hw3_provided.c).


Skim over the lab so you know what you will be doing.


Problem 1

int remove_max(int array[], unsigned int length);

In this problem, remove_max receives an array of the specified length, where length is the address of the location holding the length. The function is supposed to find the maximum item and remove it from the array and return the value.

For example, if the length begins at 5 and array contains: 3, 2, 8, 5, 4, then the function will return 8 and, upon return from the function, array will contain: 3, 2, 5, 4. Note that the array actually still has 5 things in it - we will just ignore whatever is in the 5th spot. The problem is, how do we decrement the length? We are returning the max value. In this case, the calling program is going to need to update the length. This is clearly an imperfect solution, which we will fix shortly.


Problem 2

void make_horizontal_stripes(
    unsigned int red[ROWS][COLS],
    unsigned int green[ROWS][COLS],
    unsigned int blue[ROWS][COLS],
    unsigned int stripe_height,
    unsigned int stripe_red,
    unsigned int stripe_green,
    unsigned int stripe_blue,
    unsigned int width,
    unsigned int height);

In this problem, the red, green, and blue arrays are out parameters. They will contain the pixels for a width x height striped image. It will contain horizontal stripes, in which every stripe has width stripe_height. The color of the top stripe is (stripe_red, stripe_green, stripe_blue). The color of the next stripe is black. They alternate from there.

If stripe_height is 0, then the entire picture is black.
If width > COLS or height > ROWS, then print out an error.

The result of make_horizontal_stripes(r, g, b, 1, 30, 144, 255, 12, 12) is at horiz_stripes.html. This corresponds to horiz_stripes.png.


Problem 3

void make_checker_board(
    unsigned int red[ROWS][COLS],
    unsigned int green[ROWS][COLS],
    unsigned int blue[ROWS][COLS],
    unsigned int square_width,
    unsigned int square_red,
    unsigned int square_green,
    unsigned int square_blue,
    unsigned int width,
    unsigned int height);

In this problem, you will fill in a picture with dimensions widthxheight. The red, green, and blue arrays are again out parameters. They will contain the pixels for a width x height checkerboard image. In the checkboard pattern, each square has width and height square_width. The color of one square is (square_red, square_green, square_blue). The color of the other square is white.

If square_width is 0, then the entire picture is white.
If width > COLS or height > ROWS, then print out an error.

The result of make_checker_board(r,g,b,4,30,144,255, 12, 12) is at checkerboard.html. This corresponds to checkerboard.png.

Problem 4

void mortgage_calculator(unsigned int dollars, double interest, unsigned int init_months, unsigned int months_passed, double *monthly_payment, double *loan_balance);

Pointers can be used as in parameters (sending information in) and out parameters (to be filled in by the program). In this exercise, you will use your code from the mortgage calculator in hw1. Instead of having two separate functions that return one value each, we use out parameters to allow a single function to calculate both values.

In this case, we pass in all of the information necessary for both calculations, then we pass in two additional variables - pointers to locations where we want the answers placed. monthly_payment and loan_balance are out parameters.


Problem 5

int remove_max_in_out(int *array, unsigned int *length);

The problem with the original remove_max function is that the length needed to be decremented outside of the function - there was no way to return both the value of the max item and the new length of the array (without structs, which we haven't covered yet). However, we can use pointers to allow remove_max to update length.

The same parameter can be used as both an in parameter (providing information to the function) and an out parameter (providing a location to pass information out of the function).

In this problem, remove_max_int_out receives an array of the specified length, where length is the address of the location holding the length. The function is supposed to find the maximum item and remove it. By removing it, we mean to modify the array and the length.

For example, if the length begins at 5 and array contains: 3, 2, 8, 5, 4, then upon return from the function, length will contain 4 and the array will contain: 3, 2, 5, 4. Note that the array allocation does not change. There is still a 5th spot, but we don't care what it holds. We are (incorrectly) reporting the length to be 4, so those are the only locations that matter.

In this case, both array and length are in and out parameters.