Processing images may seem like a daunting task. With thousands of pixels to keep track of, they can be downright scary to the occasional programmer. But when you stop and think about it, they break down to a basic data structure that you know how to use.
Before we get to any code, let’s break these images down. To start with, let’s use a grayscale picture (it’ll be easier). Each of the pixels in a grayscale picture has one value, the gray value. If you think about pixels as numbers instead of a region of color, what do you get? A 2D array. This 2D array is stored in basically a text file. All you have to do is read in the array into an array you create.
Now that you know that images are basically 2D arrays, you’ll need to know how to create an image object in c++.
First you’ll need to create a user defined class. If you don’t know how to do this, you can Google it or look at the Tutorial here on DIC (http://www.dreamincode.net/forums/showtopic9870.htm , written by Born2Code).
In the class you’ll want to have the 2D array. You’ll also want variables holding the number of rows and columns and the max gray value. These will help when you create the object. In the rest of the class you’ll want functions that you’re going to use to process the image, such as rotating and enlarging.
Here is the class that we will be working with:
#ifndef IMAGE_H #define IMAGE_H class Image { public: Image(); //constructor Image(int numRows, int numCols, int grayLevels); //constructor ~Image(); //destructor Image(const Image& oldImage); //constructor void operator=(const Image&); //overloaded assignment operator void setImageInfo(int numRows, int numCols, int maxVal); void getImageInfo(int &numRows, int &numCols, int &maxVal); int getPixelVal(int row, int col); void setPixelVal(int row, int col, int value); bool inBounds(int row, int col); //checks to see if a pixel is in bounds void getSubImage(int upperLeftRow, int upperLeftCol, int lowerRightRow, int lowerRightCol, Image& oldImage); void enlargeImage(int value, Image& oldImage); void shrinkImage(int value, Image& oldImage); void reflectImage(bool flag, Image& oldImage); void translateImage(int value, Image& oldImage); void rotateImage(int theta, Image& oldImage); Image operator+(const Image &oldImage); //overloaded + operator Image operator-(const Image& oldImage); //overloaded - operator void negateImage(Image& oldImage); private: int N; // number of rows int M; // number of columns int Q; // number of gray levels int **pixelVal; //2D array }; #endif
(By the way, we will be working with 3 files: main.cpp, image.h, image.cpp. They will be attached as .txt files.)
Let’s start with Image(int numRows, int numCols, int grayLevels). Very rarely will you create an Image without knowing the rows, columns and the max gray level, so I won’t go into Image(). Don’t worry it’ll be in the code.
The constructor Image(int numRows, int numCols, int grayLevels) will create an Image with dimensions numRows x numCols with the maximum gray level of grayLevels. The code is below:
Image::Image(int numRows, int numCols, int grayLevels) /* Creates an Image of numRows x numCols and creates the arrays for it*/ { N = numRows; M = numCols; Q = grayLevels; pixelVal = new int *[N]; for(int i = 0; i < N; i++) { pixelVal[i] = new int [M]; for(int j = 0; j < M; j++) pixelVal[i][j] = 0; } }
(This code goes into image.cpp)
This sets the private data members of the class and creates the array. As I said before, we are using an array of pointers that point to integer arrays. First we create that array of pointers. Each pointer is a row of the array. Then in each of those rows, we put enough memory to hold the cells of that row. After you create the arrays, you can use the [][] operators. Then we set the image to a blank black image (you can also insert Q to make a white image).
After we create the object, now we can modify and insert the data from the file.
The type of Image that we will be using in this example is the .PGM file format. For more info on this format, go to http://tinyurl.com/pgmfile. I’ll sum it all up real quick. The file holds all you need to know to create the image. The type of image, also called the magic number (will be P5); the file name (we won’t care about this line); the number of rows and columns; and the data (2D array).
To read this into the image we will write a function to do this. I put this into main.cpp, but you can put this into the class very easily with little modification. The code is below:
int readImage(char fname[], Image& image) { int i, j; int N, M, Q; unsigned char *charImage; char header [100], *ptr; ifstream ifp; ifp.open(fname, ios::in | ios::binary); if (!ifp) //error checking { cout << "Can't read image: " << fname << endl; exit(1); } // read header ifp.getline(header,100,'\n'); //magic number if ( (header[0]!=80) || (header[1]!=53) ) //if not P5 { cout << "Image " << fname << " is not PGM" << endl; exit(1); } ifp.getline(header,100,'\n'); while(header[0]=='#') //file name line in file starts with # ifp.getline(header,100,'\n'); M=strtol(header,&ptr,0); //number of colums N=atoi(ptr); //number of rows ifp.getline(header,100,'\n'); Q=strtol(header,&ptr,0); //max gray value charImage = (unsigned char *) new unsigned char [M*N]; //creates 2D array ifp.read( reinterpret_cast<char *>(charImage), (M*N)*sizeof(unsigned char)); //reads in 2D array if (ifp.fail()) { cout << "Image " << fname << " has wrong size" << endl; exit(1); } ifp.close(); // Convert the unsigned characters to integers int val; for(i=0; i<N; i++) for(j=0; j<M; j++) { val = (int)charImage[i*M+j]; image.setPixelVal(i, j, val); } delete [] charImage; return (1); }
I have inserted comments in the code so the code is explained right when you first read it. Using these same concepts you also need a function to write to the file, which is included in the main.txt (main.cpp file) that is attached.
However, there are limitations on this function. The major thing is that you pass the image already at the size it should be at. To fix that, you have a function that reads just the header on the image file. This goes into main.cpp also:
int readImageHeader(char fname[], int& N, int& M, int& Q, bool& type) { int i, j; unsigned char *charImage; char header [100], *ptr; ifstream ifp; ifp.open(fname, ios::in | ios::binary); if (!ifp) { cout << "Can't read image: " << fname << endl; exit(1); } // read header //************************* //this section will not be used in this tutorial, this is used for programs that can handle color images type = false; // PGM ifp.getline(header,100,'\n'); if ( (header[0] == 80) && (header[1]== 53) ) { type = false; } else if ( (header[0] == 80) && (header[1] == 54) ) { type = true; } else { cout << "Image " << fname << " is not PGM or PPM" << endl; exit(1); } //***************************** ifp.getline(header,100,'\n'); while(header[0]=='#') ifp.getline(header,100,'\n'); M=strtol(header,&ptr,0); N=atoi(ptr); ifp.getline(header,100,'\n'); Q=strtol(header,&ptr,0); ifp.close(); return(1); }
For this program, bool type does nothing. This will set the number of rows, cols, and max grayscale value that you need to create the image. At this point you have written all the functions you need to create the object. The code below puts it all together. This is in int main().
int main(int argc, char *argv[]) { int M, N, Q; // rows, cols, grayscale int val; bool type; // read image header readImageHeader(argv[1], N, M, Q, type); // allocate memory for the image array Image image(N, M, Q); // read image readImage(argv[1], image); }
If you’re not quite sure what int argc, char *argv[] is, Google it. They refer to parameters passed to the program.
Congratulations, you did it! You read in the file, but now is the fun part: manipulation.
Since the data in the Image is a 2D array, much of the manipulation is simple and easy to figure out. I won’t go into most of the functions, and they will be attached anyways. But try to write your own code for them, it’s a lot of fun to see the code your write do stuff!
But anyways, I will go into detail about one: rotateImage(int theta, Image& oldImage).
There are a lot of things you will have to think about with rotating the Image. You need a little math to help you out. There are formulas that you will need to use to get it right.
r’ = r cos(theta) – c sin(theta) c’ = r sin(theta) – c cos(theta)
But to rotate it around the center you will need to use:
r’ = r0 + (r – r0)cos(theta) – (c – c0)sin(theta) c’ = c0 + (r - r0)sin(theta) + (c – c0)cos(theta)
Note: r0 and c0 are the center points.
Below is the actual code to do this operation (goes into image.cpp):
void Image::rotateImage(int theta, Image& oldImage) /*based on users input and rotates it around the center of the image. { int r0, c0; int r1, c1; int rows, cols; rows = oldImage.N; cols = oldImage.M; Image tempImage(rows, cols, oldImage.Q); float rads = (theta * 3.14159265)/180.0; //converts the degree given by user into radians //find midpoints r0 = rows / 2; c0 = cols / 2; // goes through the array of the oldImage, uses the formulas to find where the pixel should go // then puts the old pixel value into the new pixel position on the tempImage for(int r = 0; r < rows; r++) { for(int c = 0; c < cols; c++) { r1 = (int) (r0 + ((r - r0) * cos(rads)) - ((c - c0) * sin(rads))); c1 = (int) (c0 + ((r - r0) * sin(rads)) + ((c - c0) * cos(rads))); if(inBounds(r1,c1)) // makes sure the new pixel location is in bounds, { tempImage.pixelVal[r1][c1] = oldImage.pixelVal[r][c]; } } } for(int i = 0; i < rows; i++) { for(int j = 0; j < cols; j++) { if(tempImage.pixelVal[i][j] == 0) tempImage.pixelVal[i][j] = tempImage.pixelVal[i][j+1]; } } oldImage = tempImage; }
Now many of you might be wondering what the last for loop is for. See the formulas present an interesting problem. You would find that due to the integer truncation, some of the pixels on the new image would not be touched, leaving little holes (you should remove that for loop and see for yourself). To counter act that, I have put in the last for loop. It goes through the array and if the pixel is black has a neighbor of the right side, it will assume the color of the neighbor.
That wraps up this tutorial. Attached are the original files used for the project and the rest of the code. I have elected to not talk about a lot of the functions here. They are in the attached files. I believe that if you understand that the 2D array is easy to manipulate, you can do all of the functions easily. Sorry, the files don’t have all the comments that I put in here.
Thanks, and have fun manipulating those images!
Attached File(s)
-
image.cpp.txt (8.45K)
Number of downloads: 17080 -
image.h.txt (1.46K)
Number of downloads: 13061 -
main.cpp.txt (11.71K)
Number of downloads: 14016