Ever play with Photoshop? Or GIMP? They allow you to load an image and then maniplulate it. Want to recolor? Want to resize or rotate? They can do that and much else too.
In this project you'll use Python to create some basic image filters. These include remove red, green or blue, render in greyscale or black and white, rotate and resize. You'll also build a simple CLI (command line interface) that will allow you use the console to load an image and then apply filters of your choice.
You'll use the ansel module to create your filters. Go there. Get acquainted. Like the euclid module, it's built on the back of pygame.
Once you're ready to begin the project, go here. Fork. This will become your completed project.
Your primary task is to create a set of filters (one of which I've written for you). You'll also construct a simple command line interface (CLI) that will allow to the user to load and save images, apply filters, undo and redo, etc.
When you fire up the finished program, it should ask the user for the name of an image to load. The program will then load that image and display it. (What image? Your choice, but make it school appropriate.) Next ask the user for a filter to apply. Apply that filter and then ask for another. Inform the user if a command is not recognized or cannot be applied. Continue until the user types "quit".
The filters I wish you to write are listed below. (In the next section, I'll give you a bit of help about how to write them.)
"rr", "rg", "rb": remove red, green or blue
"grayscale": render in grayscale
"bw": render in black and white
"sepia": render in sepia
"neg": produce the photo-negative
"resize": resize the image by a scale factor given as a percent. The user might for instance type in resize 50, which would result in a half size image.
(added 11.25.2024) "zoom": zoom into an image by a given scale factor at a given point
"tile": replace image with four tiled copies (see example below)
"cw", "ccw": rotate 90° either clockwise or counter-clockwise
"glass": alter image so that it looks as if it's seen through thick glass
"8bit": make an image look as if it's from the 8-bit era
(extra credit) "sobel": implement the Sobel edge detection algorithm
The last filter is your choice. Make it creative.
The CLI should also recognize a few simple commands named below. Check the official Ansel documentation for the first three.
"load": load and then display an image. If changes have been made but not saved, inform the user and ask if she really wants to load.
"save": save the current image. Save will overwrite a file. Ask the user if she really wants to do this.
"saveas": save the current image with a new name. The user will first type in "saveas", and after they do, ask the for the file name. Tell them to include the image extension, like ".jpg" or ".png". Also tell the user, when you ask for the file name, that if they type in "x", no file will be saved.
(extra credit) "undo", "redo": undo the filter most recently applied, redo the most recent undo if the undo was the previous command executed
"help": print a list of all filters and commands
"quit": exit the program. If the current image has been altered but not saved, ask the user if she wants to save before she quits.
How will your organize your code? Obviously, each of the options above (remove red, remove green, remove blue, photo-negative, etc.) should be wrapped up in its own function. You should also write a function, named perhaps CLI (short for Command Line Interface) that runs the show. Call CLI to begin; CLI will then ask for input and respond to it. It should continue until the user types "quit".
As always, your code should be robust. I'll try to crash it. If I do, you're at fault, not me.
How will you write the code for these various filters? You'll have to do a bit of research. But I'll give you a little help.
In a greyscale image, the red, blue and green values of each pixel are the same. This means that there are only 256 shades of grey.
Google "sepia rgb" and use the formula you find.
You might find that your grayscale and sepia filters are quite fantastically wrong for certain portions of an image, or even perhaps for all of it. Here's what likely gone wrong: at some point, you tried to set red, green or blue to a value over 255. Since ansel only allows color values up to 255, this will result in an overflow. (See here.) What's the solution? Likely for grayscale you computed the average of red, green and blue for each pixel. If so, you likely computed the average as
(r + g + b) / 3. The sum inside parentheses is the culprit here; for many values of r, g and b, it will exceed 255. Can you think of a way to get the average but not compute a sum that might exceed 255? Here's a hint: distribution.
For sepia, you likely used a formula that was a kind of weighted average of r, g and b values. If so, those weighted averages are floats. Before you use them to create a new pixel, you need to check whether they're over 255. If they are, you need to set them to 255.
By "black and white", I mean an image each of whose pixels is either pure black - (0, 0, 0) - or pure white - (255, 255, 245). My black and white filter first converted the image to greyscale.
I have a suggestion: create the new, empty image of the appropriate size and then, as you scan through its pixels, do a computation to determine which pixel from the original image to bring over to the new image.
The zoom filter requires three arguments: a scale factor for the zoom, and a pair of values that specify the center of the zoom. Each of the three is given by a float between 0 and 1; I'll call them "sf", "xc" and "yc" respectively. We multiply image width by xc to get the column value of the center of the zoom, and we multiply image height by yc to get the row value of the center of the zoom. sf tells us how "deeply" we zoom. If, for instance, sf is 0.5, we "blow up" a portion of the image by a factor of 2; if it is 0.1, we "blow up" by a factor of 10.
One might think of a zoom as a pair of successive operations - a cut and then a scale. We first cut out a portion of the image whose dimensions are proportional to those of the original image. xc and yc give us the center point of the cut-out as described above, and sf gives us the size of the cut-out. For instance, if xc and yc are both 0.5, the center of the cut-out is the center of the original image; and if sf is 0.5, the width and height of the cut-out are half that of the original image.
Another example: if xc is 0.3 and yc is 0.6 and w and h are the width and height of the original image respectively, then the center of the cut-out is (0.3 * w, 0.6 * h). Moreover, if sf is 0.2, this means that the cut-out has width and height that are 0.2 times the width and height of the original image.
After we cut out a portion of the image, we then resize the cut-out so that its dimensions equal those of the original image. Call your resize filter to do this. Expressed as a percent, the scale factor of the resize will be (1/sf) * 100.
To rotate, you don't simply exchange the row and column values for a pixel. That doesn't rotate; instead it flips over a diagonal line that runs from bottom left to upper right. (I'll convince you of this in class. I'll flip a bit of text.) How then do you rotate?
First point: the rotated image will exchange width and height; that is, width becomes height and height becomes width.
Second, let us consider a pair of transformations of a rectangle. The two I mean are a horizontal flip and a diagonal flip. (A horizontal flip is a flip over a horizontal line from midpoint of left side to midpoint of right side. It takes top to bottom and bottom to top. A diagonal flip is a flip over a line that runs from top left to bottom right. It exchanges the bottom left and top right corners.) If we do a horizontal flip and then follow that with a diagonal flip, the result is the same as a 90 degree clockwise rotation.
Now consider a vertical slip and a diagonal flip. (A vertical flip is a flip over a vertical line. It takes right to left and left to right. ) If we do a vertical flip followed by an upward diagonal flip, the result is the same as a 90 degree counterclockwise rotation.
When you look at a scene through a thick sheet of glass, it becomes blurred and edges that were sharp become fuzzy. How will you make this happen? Experiment!
In 8-bit RGB, we have 3 bits for red, 3 for green and 2 for blue. 2³ = 8 and 2²=4; so in 8-bit RGB, we have 8 reds, 8 greens and 4 blues. These combine to give us 2³·2³·2² = 256 possible colors. How do we make 24-bit RGB (which is the current standard) look as if it's 8-bit? We take the average of the red, green and blue for each 8-by-8 block of pixels in the input image, find the closest 8-bit red, green and blue for those, and then write a pixel with those 8-bit colors 64 times over in that 8-by-8 block. How do we get the 8-bit colors? For red and green, we limit the range 0 - 255 to eight possible values: 0, 36, 72, 109, 145, 181, 218, 255. For blue, we limit the range 0 - 255 to four possible values: 0, 85, 170, 255. Thus we must take the actual red and green and round to the nearest value in the first list; and we must take the actual blue and round to the nearest value in the second list. We then take these rounded values and make a new pixel. For instance, color (66, 100, 212) would become (72, 109, 170). (You'll no doubt want a formula. Hint: divide 256 by 7, divide 256 by 3.)
When you Google this, you'll find a formula that makes use of matrices and performs a certain operation of them. So, you'll likely have to teach yourself about that. This will be a real challenge, I expect.
Please refer to the official Euclid documentation.
The user should be able to undo back to the original image. Redo should be possible only if the command entered immediately before was "undo"; and the number of "undo"s done immediately before should be the number of "redo"s that are possible. How will you handle this? I suggest two lists: an undo list and a redo list. Begin the undo list with the image that's been loaded; and every time an image is altered, place the new version in the undo list. When the user chooses "undo", display the next to last image in the undo list, and move the last image into the redo list. When the user chooses "redo", display the last image in the redo list, and move it to undo. Moreover, any time the user chooses a filter, empty the redo list; a redo is possible only if the previous command was undo. Of course I'll expect that the CLI won't crash if the user chooses "undo" or "redo" when it isn't possible. For instance, if the user loads an image, then applies "sepia", and then tries two "undo"s, the CLI should inform the user after the second that it isn't possible.
Here's a copy-paste from my CLI. I thought it might help.
Enter image name with extension: fry.jpg
Command or filter: double
Command or filter: quit
Image not saved. Really quit (y or n)? n
Command or filter: undo
Command or filter: resize 200
Command or filter: 8-bit
Command or filter: rr
Command or filter: saveas
Enter image name with extension or x to exit: new_fry.jpg
Command or filter: quit
Remove Red
Remove Green
Remove Blue
Grayscale
Black and White
Sepia
Negative
Tile
Clockwise
Counterclockwise
8-bit
Sobel