PhotoChop
PhotoChop
This project puts together different concepts from throughout the course and gives you experience going into a codebase and working with code you did not write, which is what most software developers spend most of their time doing. The codebase you are given is a simplified image editor, which a collection of functions that allow opening, creating, manipulating, and saving bitmap images.
Your assignment will be to make two new functions, one each from two different categories explained below. If you want to experiment further, you can make more than two functions, as long as there are at least two that are in different categories.
Background
Images
We can think of an image as a grid of squares called pixels. Each pixel has a color value that can be represented as a mix of red, green and blue. If we allow each of these three color ‘channels’ to have a value of 0–255, i.e. 8 bits (1 byte) of storage, the color of one pixel is represented with 24 total bits (3 total bytes), allowing for over 16.7 million different colors.
If all three channels have the same value we get a shade of gray, ranging from 0,0,0 (black) to 255,255,255 (white). If some channels are different, we will get a color determined by the mix of the three. In general, higher numbers are brighter, and smaller numbers are darker.
Codebase
For starter code, use Week10/PhotoChop in your copy of the cs161Code files. Included are the existing codebase, some sample images, and a VS Code setup so that you can build and run what's there. Start by getting that working and looking around at what is there before you start making changes.
Treat the ImageIOLib.h
and ImageIOLib.cpp
files as black boxes—you
do not need to worry about them. If you're curious about how the program
is reading in image files, you can have a look inside, but it is not
part of the assignment.
The Image.h
header declares important data types and constants.
typedef uint8_t byte;
The uint8_t
type, provided in the cstdint
header, is an unsigned
integer 8 bits wide—just the right size for storing a color channel—so
there is a typedef alias byte
for us to use wherever in the code we
want such a value.
struct Pixel {
byte red;
byte green;
byte blue;
};
A Pixel
is a structure that holds three of those values together,
representing a complete color.
const int IMG_HEIGHT = 128;
const int IMG_WIDTH = 128;
To keep the codebase simple, it only works with images of a particular, hardcoded size, in this case 128×128 pixels.
struct Image {
Pixel data[IMG_HEIGHT][IMG_WIDTH];
};
An Image
is a structure that uses a two-dimensional array of Pixel
s
to represent a complete image.
The file main.cpp
has code to create some images, as well as to load
an image and modify it. Use the existing code as a model for how to
write your own functions that do similar work.
Assignment Instructions
Submit file: main.cpp
Make two or more functions that create an image or modify an existing image using the PhotoChop code. You must make your functions from at least two different categories listed below (for example, you can't count two ‘Create New Image’ functions).
Add your new functions to main.cpp
, and change the main
function in
that file to show them off. When you are done, submit your main.cpp
file in elearn. If you have created or used any sample images other
than the ones provided with the codebase, you may submit them as well.
This codebase is not set up to record a work history; you are free to initialize it as a git repository and record history for yourself, but you are not required to submit it for this assignment.
Function ideas
Here are some ideas for possible functions, but feel free to invent your ideas or modify these suggestions.
Drawing a pattern
In this category, your function creates a new Image
, filled in with some
interesting contents (e.g. makeAqua
and makeGradient
in main.cpp
).
Box
Write a function that takes in parameters for starting row, column, width, and height. It then uses those values to draw a white box (or whatever color you like) on a black background.
Color gradient
Blend from one color to another.
Circle gradient
Calculate each pixel's RGB values based on the distance from the center of the image. Set each pixel to a gray value (same value for all three channels) based on that pixel's distance from center.
Snow
For each pixel in a new image, pick a random number from 0–255 and use that for all three color values.
Modifying an image
In this category, your function takes an existing Image
and adjusts
each pixel independently (e.g. redShift
in main.cpp
).
Only red
Set the blue and green value for each pixel to be 0.
Grayscale red
Set the blue and green values for each pixel equal to their red value. (Or set red and blue equal to green, or set all three to the average of their values; each of those approaches produces a different kind of grayscale image.)
Bluescreen
Identify any pixel where the blue value is higher than the red value. Turn
those pixels to white (255,255,255). This filter should be able to clear
most of the background in crab.bmp
.
Crop center
Make a black or dark border around the edge of the image, only leaving the middle visible.
Invert
Set each color to be 255 minus the original value.
Darken or lighten
Multiply the red, green and blue values each by a value. A fraction between 0 and 1 will darken the image; a value > 1 will brighten the image.
Noise
For each color of each pixel, pick a random number from -20 to 20 and add it to the color value.
Moving pixels
In this category, your function will take an existing image and
change it by rearranging the existing pixels into different locations
(e.g. rotateRight
in main.cpp
).
Flip vertical or horizontal
Flip the image in one dimension or the other (left-right or up-down).
Rotate left
Make a rotation that goes the opposite direction from the provided
rotateRight
.
Combining pixels
In this category, your function will take an existing image and combine
multiple pixels from it to change its appearance (e.g. blur
in
main.cpp
)
Sharpen
Calculating a ‘blended value’ that assigns negative weight to neighboring values will emphasize differences between neighbors and ‘sharpen’ an image by making edges look more crisp.
For each color, multiply the current pixel's value by 5 and neighboring pixels' values by -1. Add them all up–this is the new value for the current pixel.
Using 9 for the current pixel and -2 for the neighbors makes for a stronger effect. Or you can use a larger neighborhood. Just make sure the multipliers add to 1 overall if you don't want to change the brightness.
Find edges
If you write a sharpen filter but make the current pixel multiplier value 4 instead of 5 (so that the multipliers add to 0 instead of 1), it will be an edge-detection filter that shows solid areas of color as black and areas with sharp changes as white.
8-bit look
Copy pixels that are on an even row and column into their ‘odd-numbered’ neighbors (odd-numbered rows and columns from original are not used).
Zoom
Stretch one quarter of the image over the entire space. To ‘zoom in’ on the upper left corner of the image, you would take the values from that corner and ‘stretch’ them so that each pixel in the original becomes 4 pixels in the modified version.
Combining images
In this category, your function will take multiple Image
parameters
and combine them into a new result.
Merge
Read in two separate images. Write a function that averages the values of corresponding pixels to create a new image. Or use the top half of one and the bottom of anther. Or alternate lines. Or blend so that you use 100% of one image at the top, 100% of the other at the bottom and gradually shift the weights like a gradient.
Secret message
There is a
secret message in crabMessage.bmp
. Compare the pixels
in it to the pixels in crab.bmp
using a filter that produces a white
pixel where the images have different green values, and black where they
have the same green values.
Optional extra information about bitmap images
Data files are often stored in binary format. It is much more compact to store the number 200000 as a 32-bit integer than as those six characters. It is also faster to read in those raw bits instead of reading strings and converting them to integers.
To read binary-formatted files humans usually use a hex editor, which shows each byte of the file as a character.
File formats specify how a particular kind of file is to be written—how to interpret the 1's and 0's. Below is a simplified description of the file format for .bmp (bitmap) image files.
Byte Address | Description | Sample value from above screen shot |
---|---|---|
0 | Magic Number - verifies really is bmp data - always 66 (42 hex) | 42 hex (66 in decimal) - represents character 'B' |
1 | Magic Number - verifies really is bmp data - always 77 (4d hex) | 4d hex (77 in decimal) - represents character 'M' |
10 | Byte address image data starts | Look in column 10 in the first row. The value is 36hex or 54 in decimal. The image data starts at byte #54. |
18 | Width of bitmap | Find the 19th byte (address 18) - it is the third one in the second row. The value of 80 means the width is 80 in hex or 128 in decimal. |
22 | Height of bitmap | The height (also 80 hex/128 decimal) can be seen in byte #22 (halfway across second row). |
The ImageIOLib functions do the work of parsing the file and reading the image part of the data into an array.
It is possible to supply your own pictures as input to PhotoChop! First, open the image you want to use in paint, gimp, photoshop, etc. Save as bmp.
If you have options, use 24 bit color.
Make sure that the image you use is the same dimensions as specified by
the constants in Image.h
. It is possible to change those constants
to match, but some of the filters might not work correctly (the supplied
gradient function, for example, will wrap around, and the supplied rotate
function only makes sense for square images).
Have fun!