Step 6: Working with an Image

In this step you will work with a very simple image structure in memory.  The idea is to get used to manipulating pixels and dealing with rows and columns of an image.  You'll manually create some very basic images.

To do this step, you will need to download and compile the project in SimpImage.zip.    Be sure you have a copy and can get it to compile.  It does not do very much right now.

The first parts of this step are just to get you used to working with images in arrays.  After that, there are two tasks that load a real image from a file and manipulate it.  These are actually quite easy to do, but have been flagged as optional in the Step.  I do encourage you to do them, though.

What SimpImage does

The SimpImage application is just about the easiest example of manipulating an image you will ever see.  If you will look at CSimpimageDoc, you'll find a member variable like this:

    float m_image[IMAGEHIT][IMAGEWID];

This variable is an array that holds a simple image.  IMAGEHIT and IMAGEWID are both constants with values of 512, so this is a 512 by 512 image.  This results in an image that consists of 262,144 pixels (picture elements).  Each of these pixels is represented in the array by a float value.  Increasing values are brighter shades of gray.  The minimum value is 0.0, which is a black pixel.  The maximum value is 1.0, which is a white pixel.  Note the normalization here.  The maximum is NOT 255 or some value based on the size of a byte.  It is 1.0.  This is very common in applications that utilize floating point values to represent image pixels.  (You might want to think about why.)

The array is a 2D array.  The outer dimension is the rows of the image and the inner dimension is the columns.  If I want to set the pixel at row 25, column 77 to white, I would do:

    m_image[25][77] = 1.0;

The format of the array is top-down.  The first row (row 0) is the top row of the image on the screen.

My CSimpimageView class simply displays the image on the screen.

Caveats

This is the most simple image data structure that I can think of.  As such, it has advantages and disadvantages that you should be aware of. You can expect to be quizzed on this information at some point.

Fixed Array Size

The image is allocated using a fixed array.  This is actually very rarely done.  You have to know exactly how big the image is going to be to create this allocation and usually you don't.  And, this causes all kinds of problems with passing the image to functions or having them be function return values.  This is why I've made the member variable public in this program.  You'll never see this type of allocation again.

Top Down or Bottom Up?

I have chosen to store this image top-down.  That is a completely arbitrary decision.  The next step you'll work on will store images bottom up.  Both formats are very common.

Floating point values?

I'm sure some are asking why we used floating point values in this step?  I did so to keep the step easy.  You don't have to worry about numeric overflow.  And, you should realize that many commercial product (such as Photoshop) use floating point representations of images in order to avoid round off errors.

Row as the Outer Dimension?

We're used to referring to dimensions as x,y, so wouldn't it make more sense to make the column the outer dimension so we can do this?:

    m_image[x][y] = 0.55;        // Bad<<<

Actually, this is a very bad idea.  The reason is that computer displays output content a row at a time, so all of the pixels in a row are contiguous.  Hence, we want our internal representation to also be contiguous.  Even if we are not directly copying the image to a display, we don't want to be jumping through large image data as we output a column.  That would be ever ineffective from a virtual memory standpoint.  (Why?)

First Task:  Filling

I've provided a Step menu with some menu options.  One of these is Fill With Black.  If you select that option, you'll find that the display will fill with black.  What I'm doing is filling the entire array with 0.0.  Here's the code that does this:

void CSimpimageDoc::OnStepFillwithblack() 
{
   // TODO: Add your command handler code here
	
   for(int r=0;  r<IMAGEHIT;  r++)
   {
      for(int c=0;  c<IMAGEHIT;  c++)
      {
         m_image[r][c] = 0.;
      }
   }

   UpdateAllViews(NULL);
}

You'll notice that I have two nested loops for the rows and columns.  Then, I call UpdateAllViews(NULL) to cause the screen to be redrawn.  If you are interested in how that happens, take a look at OnDraw in the view class.  This is pretty basic stuff. 

Your Task:  Just for practice, go ahead and fill in the OnStepFillwithwhite() procedure and make the image all white.

Next Task:  Simple Lines

Seeing as how filling is so easy, let's try to draw some lines.  I've created menu options for drawing a horizontal line, a vertical line, and a diagonal line.  Here's an example of how to draw a vertical line:

void CSimpimageDoc::OnStepVerticalline() 
{
   for(int r=0; r<IMAGEHIT; r++)
   {
       m_image[r][256] = 1.0;
   }

   UpdateAllViews(NULL);
}

I choose the column somewhat arbitrarily. 

Your Task:  Now, modify the function to draw a black line on a white background.  Then, fill in the horizontal and diagonal line functions do they do the same thing.  For the diagonal line, make a perfect 45 degree angle line from the top left corner to the bottom right corner. 

Just so this is clear:  Each menu option should create a white image, then draw the line in black. 

Shades

We've been keeping our image data pretty much binary so far.  Let's create a gradient image.  A gradient image is an image that fades from one shade to another over the range of the image.  Here's a simple horizontal gradient example:

   for(int r=0;  r<IMAGEHIT;  r++)
   {
      for(int c=0;  c<IMAGEHIT;  c++)
      {
         m_image[r][c] = float(c) / float(IMAGEWID-1);
      }
   }

Note that all pixels on the left edge of the image (column 0) will be black and all of those on the right edge will be white. (Why did I use IMAGEHIT-1 rather than just IMAGEWID?) 

Your Task:  Put this code under a menu option and try it.  Then create two more menu options.  One should create a vertical gradient with white on the top line of the image and black on the bottom.  The other should create a diagonal gradient with white has the top-left pixel and black as the bottom-right pixel and every pixel on an 45 degree line from lower left to upper right should be the same color.  It should look like this:

Gradient image

(Hint:  Remember that if you go one step up a 45 diagonal, you are decreasing the row by one and increasing the column by one (the same about)).

Loading an Image from a File

I have placed the img.dat in the directory with SimpImage.  This rather large file contains 262,144 floating point values.  Not coincidentally, that's exactly how many pixels there are in your image.  I've stored an image in that data file.  The data is stored from top to bottom one row at a time.  The first 512 numbers are the values for the first row of the image. 

Your Task:  Write a function that will load the image from the file data.  I've provided a function OnStepLoadrawimagedata() that opens a dialog box and allows you to specify the image you are going to load.  You have to actually do something with the filename, which can be obtained from dlg.GetPathName() (see the documentation).  You can use that in an ifstream constructor:

    ifstream istr(dlg.GetPathName());

Rule:  You must use ifstream for this.  fgets, fread, and other C style input functions are obsolete and should not be used with C++.  The good thing is that all you'll have to do to read the data is:

    istr >> m_image[r][c];

But, be sure you know some things about C++:

1.  Be sure you include the header file fstream.  Do NOT include fstream.h. All of the header files with .h extensions are legacy support and are obsolete.  You should have a line like this:

    #include <fstream>

2.  Be sure you do the #include line before the #ifdef _DEBUG line, but after #include "stdafx.h".  Any header files you include before #include "stdafx.h" are ignored. 

3.  ifstream is a class in the Standard Template Library (STL).  The standardized version of STL puts these items in a namespace named std.  So, if you just do this:

    #include <fstream>

    ...

    ifstream istr(dlg.GetPathName());

you'll get an unidentified symbol error.  The reason is that you have to specify the name space to use.  You have two options:

a)  Full specify the name like this:  std::ifstream istr(dlg.GetPathName());

b)  Enable the std namespace by placing this line of code right after you include fstream:

    using namespace std;

I tend to use the latter approach quite a bit. 

Flipping an Image

The last task I'll ask is that you create a mirror image.  When you select a menu option, the image should flip as if viewed in a mirror.  Selecting the option twice should return you to the original image.  This is a mirror around a vertical line, so things should stay at the same height, but flip left to right.

Your tasks

Remember that you have the following tasks to complete during this step:

You must provide this solution to the TA. 

CSE 471