In this assignment you will complete 4 functions in a program that perform transformations to a given list of lists of ints that represents a digital image.

Start with the provided images.py program. Complete the header section and the following four functions described below. Add other functions as necessary to provide structure and remove redundancy.

Recall from the first lecture how I said in most computer systems all data is stored as numbers. Those numbers are given different meaning based on the encoding scheme used, for example ASCII or Unicode for text.

The same is true for digital images and there are many, many different encoding schemes.

Among the simplest are a series of encoding schemes and file formats under the Netpbm project. The formats are extremely simple, but are not actually used much in practice as their are other approaches that use far less memory when storing the data in a file or transmitting the information over a computer network.

This assignment deals with the plain (or ASCII) pgm file format. PGM stands for Portable Graymap. The PGM file format is explained at this web page.

  • The first line of a plain PGM file is the "magic number" P2. This indicates the file is a plain pgm file.
  • The next line of the file is an optional comment line that starts with a #.
  • The next value is a number indicating the width of the image. Digital images are typically broken up into pixels (Picture Elements). A low resolution (by today's standards) image of 640 x 480 means the image (in landscape) is 640 pixels wide by 480 pixels high. Each pixel is like a small rectangle or cell and the color (or sample) of that area is represented with a number or a series of numbers.
  • The next value, after some white space, typically on the same line is the height of the pgm image.
  • Next is a whitespace character, typically a newline and then a number that represents the maximum possible gray value. This number indicates how many shades of gray the image can contain. For example if the number was 1, then every pixel would only have 2 color options, 0 or 1, representing black or white. And yes, 0 represent black and 1 represent white. If the max gray value was 255 then the color of a pixel could vary from 0 (again black) to 255 (white) with 254 shades of gray in between.
  • Next are width * height numbers separated by whitespace. It is not required that all the pixels (or values) in a row be on the same line, but this is typical.

Here is a very simple, very small example:

P2
# Image of an H.
6 9
15
0 0 0 0 0 0
0 15 0 0 15 0
0 15 0 0 15 0
0 15 0 0 15 0
0 15 15 15 15 0
0 15 0 0 15 0
0 15 0 0 15 0
0 15 0 0 15 0
0 0 0 0 0 0

It is not required that the columns line up as shown. That was added for readability of this example.

A 6 x 9 pixel image would be tiny on most modern displays.

The code to read and write pgm files is provided in the images.py program.

Regardless of the image format, the majority of image processing programs work with the image as an image raster. The image is represented as a matrix (a list of lists in Python) where each element is a pixel and the color of that pixel is stored as a number. In this assignment we will only work with grey scale images that require a single number to represent a color. Color images often represent the color of a pixel with three numbers, the amount of red, green, and blue (RGB) in the pixel varying from 0 (none) to some maximum value (typically, but not always 255).

Once we have the list of list of ints we can alter then image by changing those numbers. Filters or transformations of images follow some set algorithm for altering the color values. On this assignment you will implement four functions, each one representing a different filter or transformation.

Each function creates and returns a new list of lists of ints the same size as the raster passed into the function. The original raster is not altered by these functions.

Consider this image of the Mona Lisa: see image.

1. The first function / filter is inverting the colors of the image. This means creating a new raster, but each element is equal to the max grey value - the original value. Here is the result of applying this filter to the Mona Lisa: see image.

2. The second function is creating a mirror image of the original raster. Essentially the order of the values in each row are reversed.

Here is the result of applying this filter to the Mona Lisa: see image.

3. The third filter is a blur or softening. This is a neighborhood filter where thee value of each pixel is altered to the average (truncated integer) of the pixels neighboring it. In general each pixel has 8 neighbors, the pixels above, below, left, right, and diagonal to it. There are special cases with the corner pixels (3 neighbors) and edge pixels not on the corner (5 neighbors.) This algorithm is very similar to the Game of Life example from the list of lists video.

For example if we had the pixel below with a value of 5 and the neighbors shown:

10 0 17
3 5 19
0 12 22

The sum of the values in the neighborhood is 10 + 0 + 17 + 3 + 5 + 19 + 0 + 12 + 22 = 88. The average is 88 / 9 = 9.777... This value would be truncated (using the int function) to 9.

Here is the result of applying this filter to the Mona Lisa: see image.

4. The final filter is a brightening or washout filter. The value of each pixel in the transformed image is the maximum of the pixels in the neighborhood around it. The pixel itslef could be the maximum value and actually be unchanged. The neighborhood is the same as in the blur filter described above.

Here is the result of applying this filter to the Mona Lisa: see image.

Starter Code

# File: images.py
#
# Description: Read in a plain pgm format file and apply an image
# filter to it selected by the user. Write out the new image to a pgm
# file. The program assumes files are encoded with the plain pgm
# format per http://netpbm.sourceforge.net/doc/pgm.html.

def main():
"""Process one file based on user input.

1. Get the name of the image file from the user.
2. Read in the data from the file.
3. Determine what filter to apply.
4. Apply the filter.
5. Write out the new file.
We assume the user correctly enters the name of a file
formatted in the plain pgm format"""
infile_name = input('Enter file name: ')
image_raster, max_value = read_pgm_file(infile_name)
choice = get_filter_choice()
if choice == 1:
result = invert_colors(image_raster, max_value)
else:
choices = [mirror, blur, brighten]
result = choices[choice - 2](image_raster)
print_result(result)
save_result(infile_name, max_value, result)


def get_filter_choice():
"""Ask user for which filet to apply to the current image."""
print('OPTIONS FOR IMAGE FILTER TO APPLY:')
print('1. Invert colors of image.')
print('2. Get a mirror image.')
print('3. Blur image.')
print('4. Brighten image.')
return int(input('Please enter your choice: '))


def read_pgm_file(infile_name):
"""Read in the file specified by the given file name.

We assume infile_name refers to a file in the current working
directory and that the file is in the plain pgm format.
Return an image raster (list of lists) with the values from the
file. Each row is a list in the returned value. Also return
the max value as specified by the file."""
with open(infile_name, 'r') as infile:
magic_num = infile.readline().strip()
if magic_num != 'P2':
print('First line of file not magic num P2. '
'Instead it is:', magic_num, 'Logic errors may occur')
second_line = infile.readline().strip()
if second_line[0] == '#':
print('in file contains comment:', second_line, '. Discarding.')
second_line = infile.readline().strip()
dimensions = second_line.split()
max_val = int(infile.readline().strip())
return read_raster(dimensions, infile), max_val


def read_raster(dimensions, infile):
"""Read in and return a list of lists for the values of the image.

dimensions is a string with 2 ints, the height and width of the
image. infile is the inout file. We assume the file cursor is
positioned just before the first data value after the dimensions.
Return the raster, a list of lists.
"""
cols, rows = int(dimensions[0]), int(dimensions[1])
result = [[0 for c in range(cols)] for r in range(rows)]
row = 0
current_data = infile.readline().strip().split()
current_pos = 0
while row < rows:
col = 0
while col < cols:
if current_pos == len(current_data):
# Used up the last line, need to read the next line.
current_data = infile.readline().strip().split()
current_pos = 0
result[row][col] = int(current_data[current_pos])
current_pos += 1
col += 1
row += 1
return result


def print_result(raster):
"""Print the given raster to standard output."""
for row in range(len(raster)):
print('Row ', row, ": ", raster[row], sep='')


def save_result(original_name, max_value, raster):
"""Save the raster to a plain pgm file.

raster is a list of lists of ints representing a grayscale image
in the plain pgm format. Write out the raster to a plain pgm file.
Each row is placed on a single line.
The file name is the original file name before the prefix
with _alt and then .pgm.
"""
name = original_name.split('.')[0]
name += '_alt.pgm'
with open(name, 'w') as outfile:
outfile.write('P2n')
rows, cols = get_dimensions(raster)
outfile.write(str(cols) + ' ' + str(rows) + 'n')
outfile.write(str(max_value))
outfile.write('n')
for row in raster:
output_row = ''
for value in row:
output_row += str(value) + ' '
output_row = output_row.strip() + 'n'
outfile.write(output_row)


def invert_colors(raster, maxi):
"""Create and return an inverted version of the raster.

raster is a rectangular list of lists of ints.
All values are between 0 and max inclusive. [0, max]
The returned value has each value altered to max - original_val.

Simple example, given max is initially 31
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20

would return:
30 29 28 27 26
25 24 23 22 21
20 19 18 17 16
15 14 13 12 11
"""


def mirror(raster):
"""Create a mirror image of the raster.

Raster is a rectangular list of lists of ints representing an image.

Simple example:
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20

would return:
5 4 3 2 1
10 9 8 7 6
15 14 13 12 11
20 19 18 17 16
"""


def blur(raster):
"""Create a blurred version of the raster.

Raster is a rectangular list of lists of ints representing an image.
Each value in the returned result is the average of a cell and its
8 neighboring cells. Corner cells only have 3 neighbors and edge
cells not on the corner have 5 neighbors.
"""


def brighten(raster):
"""Create a brightened version of the raster.

Raster is a rectangular list of lists of ints representing an image.
Each value in the returned result is the maximum value of a cell
and its 8 neighboring cells. Corner cells only have 3 neighbors
and edge cells not on the corner have 5 neighbors.
"""


main()
Academic Honesty!
It is not our intention to break the school's academic policy. Posted solutions are meant to be used as a reference and should not be submitted as is. We are not held liable for any misuse of the solutions. Please see the frequently asked questions page for further questions and inquiries.
Kindly complete the form. Please provide a valid email address and we will get back to you within 24 hours. Payment is through PayPal, Buy me a Coffee or Cryptocurrency. We are a nonprofit organization however we need funds to keep this organization operating and to be able to complete our research and development projects.