Date

Lecture Date: Monday, November 21

For the next few days, we are going to look at the algorithms behind some basic image manipulation.

First thing's first: we need to know how to load in a picture into our Python programs. We will be using a library called cImage that was written at Luther College specifically for use in CS 1 courses.

To get started, download these files and move them into your PyCharm project:

We are going to be working mainly with .gif files. If you would like to use something else (like .png or .jpg), you need to install the pillow Python library. (In PyCharm, open your Preferences/Settings, and use the Project Interpreter screen to install pillow, just like you did with the other libraries we have used.)

Drawing with Pixels

We started our programming journey by learning to draw pictures with the Turtle. But what exactly is a "picture" in the context of a computer program?

In effect, it's a 2D array of pixels.

Well, what's a pixel?

A pixel is the smallest display element of a picture. It could be dots on your monitor or TV, or how small an ink jet printer can put a dot on your paper (although we still typically call that "dots"). The pixel specifies what color (or components of colors) should be used to display that particular location in accordance with a color model. "Pixel" is short for "picture element."

Printers (and paints and crayons...) use a subtractive color model. That is, as you add more color (paint), more light is absorbed. Thus, adding in all the colors makes the color black, as shown in the CMYK model below.

CMYK

However, computer monitors (and all forms of light, like stage lighting) use an additive color model, like RGB.

RGB

So, as we add more and more colored lights, we get closer to white, not black!

In RGB, each of the three primary colors (Red, Green, and Blue) can have a value from 0 to 255. This allows for 16,777,216 colors (in theory)!

For example, (255,0,0) is red. But (128,0,0) is also red. It's just not a "saturated" or bright as the first red. If all three values of R, G, and B are the same, you get some shade of gray (or white or black).

Many pixels also have a fourth value - alpha. That tells the pixel how much to blend with any images behind the one that is currently "on top" and about to be drawn.

How many pixels?

It's a big question when you buy a new camera. "How many megapixels is this thing?"

For reference:

  • An old, standard TV is about 852 x 480 = 400K pixels
  • A 1080p HD TV is 1920 x 1080 = 2M pixels
  • Many laptop monitors are 1680 x 1050 pixels
  • A "retina" MacBook has 2880 x 1800 pixels
  • An 8 megapixel camera has 3264 x 2448 pixels

So, why does resolution really matter if the monitors can't display all that information? The more information you have, the more options you have, in a sense. Imagine you wanted to crop a picture. Well, with more pixels, the resulting image you end up with will still have a lot of data in it, providing a clearer picture. Also, the more pixels you have the better a print version of the image will be. This helps eliminate that "blocky" look you see when you try to print a picture at the wrong resolution.

cImage

The cImage library we will use has several objects in it that we can use to manipulate pictures:

  • FileImage - creates an image from a file you open
  • EmptyImage - creates a blank image that you can change
  • ImageWin - creates a window you can display an image inside

When you make an image you can use the following to manipulate it:

  • getPixel(col, row) - gets the pixel at that position
  • setPixel(col, row, pixel) - puts a new pixel at that position

See the example code for more examples!

Example Code

Flipping an image:

# Flipping an image
from cImage import *

# Open an image
old_image = FileImage('uva.gif')

# Open a window that is twice the size of the original image
my_image_window = ImageWin("Image Processing", old_image.width * 2, old_image.height)

# Draw the image on the window
old_image.draw(my_image_window)

# Create a new, blank image
new_image = EmptyImage(old_image.width, old_image.height)

# For each pixel in the original image...
for row in range(old_image.height):
    for col in range(old_image.width):
        # ... get the original pixel ...
        oldPixel = old_image.getPixel(col, row)

        # ... and draw it in the opposite place in the new image
        new_image.setPixel(old_image.width - col-1, row, oldPixel)

# Make sure to put the new image over to the side the right number of pixels
new_image.setPosition(old_image.width + 1, 0)

# Draw the new image
new_image.draw(my_image_window)

# Wait for a user to click the window to close the image
my_image_window.exitOnClick()

Taking the negative of an image:

from cImage import *

# Here we define a method that will take a pixel
# and return a pixel that is "flipped" as far as RGB is concerned
def negative_pixel(old_pixel):
    new_red = 255 - old_pixel.getRed()
    new_green = 255 - old_pixel.getGreen()
    new_blue = 255 - old_pixel.getBlue()
    new_pixel = Pixel(new_red, new_green, new_blue)
    return new_pixel

# Open an image
old_image = FileImage('uva.gif')

# Open a window that is twice the size of the original image
image_window = ImageWin("Image Processing", old_image.width * 2, old_image.height)

# Draw the image on the window
old_image.draw(image_window)

# Create a new, blank image
new_image = EmptyImage(old_image.width, old_image.height)

# For each pixel in the original image...
for row in range(old_image.height):
    for col in range(old_image.width):
        # ... get that pixel ...
        old_pixel = old_image.getPixel(col, row)
        # ... flip the pixel ...
        new_pixel = negative_pixel(old_pixel)
        # ... and put that pixel in the new image
        new_image.setPixel(col, row, new_pixel)

# Make sure to put the new image over to the side the right number of pixels
new_image.setPosition(old_image.width + 1, 0)

# Draw the new image
new_image.draw(image_window)

# Wait for a user to click the window to close the image
image_window.exitOnClick()