diff --git a/assignments/a1/a1.tex b/assignments/a1/a1.tex new file mode 100755 index 0000000..ac66e7c --- /dev/null +++ b/assignments/a1/a1.tex @@ -0,0 +1,98 @@ +\documentclass[fontsize=11pt]{article} +\usepackage[utf8]{inputenc} +\usepackage[margin=0.75in]{geometry} + +\title{CSC110 Fall 2021 Assignment 1: Written Questions} +\author{TODO: FILL IN YOUR NAME HERE} +\date{\today} + +\begin{document} +\maketitle + +\section*{Part 1: Data and Comprehensions} + +\begin{enumerate} +\item[1.] \textbf{Imagine this scenario...} +\begin{enumerate} +\item[(a)] +TODO: Write your answer here. + +\item[(b)] +TODO: Write your answer here. + +\item[(c)] +TODO: Write your answer here. + +\item[(d)] +TODO: Write your answer here. + +\item[(e)] +TODO: Write your answer here. +\end{enumerate} + +\item[2.] \textbf{Exploring comprehensions.} + +\begin{enumerate} +\item[(a)] +\begin{enumerate} + \item[i.] TODO: Write your answer here. + \item[ii.] TODO: Write your answer here. +\end{enumerate} +\item[(b)] +\begin{enumerate} + \item[i.] TODO: Write your answer here. + \item[ii.] TODO: Write your answer here. + \item[iii.] TODO: Write your answer here. +\end{enumerate} +\item[(c)] +TODO: Write your answer here. +\item[(d)] +TODO: Write your answer here. +\end{enumerate} +\end{enumerate} + +\section*{Part 2: Programming Exercises} + +Complete this part in the provided \texttt{a1\_part2.py} starter file. +Do \textbf{not} include your solution in this file. + +\section*{Part 3: Pytest Debugging Exercise} + +% TIP: In LaTeX, the underscore (_) is a special character, so if you want to use it +% in normal text, you have to put a backslash in front of it. E.g., a1\_part2.py, +% not a1_part2.py. + +\begin{enumerate} +\item[1.] +TODO: Write your answer here. + +\item[2.] +TODO: Write your answer here. + +\item[3.] +TODO: Write your answer here. +\end{enumerate} + +\section*{Part 4: Adding Noise to an Image} + +Complete this part in the provided \texttt{a1\_part4.py} starter file. +Do \textbf{not} include your solution in this file. + +\newpage + +\section*{Part 5: Removing Noise From an Image} + +\subsection*{Implementation} + +Complete this part in the provided \texttt{a1\_part5.py} starter file. +Do \textbf{not} include your solution in this file. + +\subsection*{Exploration} + +\begin{enumerate} +\item[1.] TODO: Write your answer here. +\item[2.] TODO: Write your answer here. +\item[3.] TODO: Write your answer here. +\end{enumerate} + +\end{document} \ No newline at end of file diff --git a/assignments/a1/a1_image.py b/assignments/a1/a1_image.py new file mode 100755 index 0000000..d63a158 --- /dev/null +++ b/assignments/a1/a1_image.py @@ -0,0 +1,49 @@ +"""CSC110 Fall 2021 Assignment 1, Bitmap Handling + +Instructions (READ THIS FIRST!) +=============================== + +Do not make changes to this file. + +Copyright and Usage Information +=============================== + +This file is provided solely for the personal and private use of students +taking CSC110 at the University of Toronto St. George campus. All forms of +distribution of this code, whether as given or with any changes, are +expressly prohibited. For more information on copyright for CSC110 materials, +please consult our Course Syllabus. + +This file is Copyright (c) 2021 Mario Badr and Tom Fairgrieve. +""" + +from PIL import Image + + +def load_image(filename: str) -> tuple: + """Return a three-element tuple containing data on the image found at filename. + + The first element of the tuple is a list of all the pixels in the image. Each pixel is a + three-element tuple that represents the red, green, and blue colour channels. + The second element of the tuple is the width of the image, in pixels. + The third element of the tuple is the height of the image, in pixels. + + Assumes that a valid image can be found at filename. + """ + image = Image.open(filename) + + pixel_data = image.load() + width, height = image.size + pixel_1d = [pixel_data[x, y] for y in range(height) for x in range(width)] + + return pixel_1d, width, height + + +def save_image(filename: str, pixels: list, width: int, height: int) -> None: + """Create a width by height image containing pixels and save it to a file called filename. + """ + image = Image.new('RGB', (width, height)) + + [image.putpixel((x, y), pixels[x + y * width]) for x in range(width) for y in range(height)] + + image.save(filename) diff --git a/assignments/a1/a1_part3.py b/assignments/a1/a1_part3.py new file mode 100755 index 0000000..c4681e6 --- /dev/null +++ b/assignments/a1/a1_part3.py @@ -0,0 +1,109 @@ +"""CSC110 Fall 2021 Assignment 1, Part 3: Debugging Exercises + +Instructions (READ THIS FIRST!) +=============================== + +This Python module contains the program and tests described in Part 3. +You can run this file as given to see the pytest report given in the handout. +Your task is to fix all errors in this file so that each test passes +(see assignment handout for details). + +Copyright and Usage Information +=============================== + +This file is provided solely for the personal and private use of students +taking CSC110 at the University of Toronto St. George campus. All forms of +distribution of this code, whether as given or with any changes, are +expressly prohibited. For more information on copyright for CSC110 materials, +please consult our Course Syllabus. + +This file is Copyright (c) 2021 David Liu, Mario Badr, and Tom Fairgrieve. +""" +import math +import pytest + + +############################################################################### +# Professor Xavier's program +############################################################################### +def class_average(class_grades: list) -> float: + """Return the weighted average grade of students for all grades in class_grades. + + Each element of class_grades is itself a list, containing three floats + representing the grades of a particular student on the three assignments. + + See assignment handout for details. + + You may ASSUME that: + - class_grades is non-empty + - class_grades contains only lists + - the lists in class_grades contain exactly three floats + - each float in each list is between 0.0 and 100.0. + + """ + student_averages = [student_average(grades) for grades in class_grades] + + # Return the average grade across all students in this section + return sum(student_averages) / len(student_averages) + + +def student_average(grades: list) -> float: + """Return the weighted average of a student's grades. + + You may ASSUME that: + - grades consists of exactly three float values + """ + # Sort the student's grades + sorted_grades = sorted(grades) + + # These are the weights for the assignment grades + weights = [0.4, 0.35, 0.25] + + return ( + weights[0] * sorted_grades[0] + + weights[1] * sorted_grades[1] + + weights[2] * sorted_grades[2] + ) + + +############################################################################### +# Tests for section_average +############################################################################### +def test_section_average_all_grades_equal() -> None: + """Test section_average when students have the same grade on each assignment. + """ + grades = [[80.0, 80.0, 80.0], + [90.0, 90.0, 90.0]] + + expected = 85.0 + actual = class_average(grades) + assert math.isclose(actual, expected) + + +def test_class_average_no_grades_equal() -> None: + """Test class_average when every grade is different. + """ + grades = [['60.0', '70.0', '75.0'], ['80.0', '65.0', '85.0']] + + expected = 73.875 + actual = class_average(grades) + assert math.isclose(actual, expected) + + +def test_class_average_many_students() -> None: + """Test class_average when there are a lot of students in a section. + """ + grades = [[80.0, 70.0, 75.0], + [90.0, 78.0, 65.0], + [66.0, 74.0, 60.0], + [60.0, 55.0, 75.0], + [82.0, 80.0, 88.0], + [50.0, 88.0, 73.0]] + + expected = 74.15 + actual = class_average(grades) + assert math.isclose(actual, expected) + + +if __name__ == '__main__': + pytest.main(['a1_part3.py', '-v']) diff --git a/assignments/a1/a1_part4.py b/assignments/a1/a1_part4.py new file mode 100755 index 0000000..2d3b572 --- /dev/null +++ b/assignments/a1/a1_part4.py @@ -0,0 +1,115 @@ +"""CSC110 Fall 2021 Assignment 1, Part 4 + +Instructions (READ THIS FIRST!) +=============================== + +Please follow the instructions in the assignment handout to complete this file. + +Copyright and Usage Information +=============================== + +This file is provided solely for the personal and private use of students +taking CSC110 at the University of Toronto St. George campus. All forms of +distribution of this code, whether as given or with any changes, are +expressly prohibited. For more information on copyright for CSC110 materials, +please consult our Course Syllabus. + +This file is Copyright (c) 2021 Mario Badr and Tom Fairgrieve. +""" +import random + +import a1_image + + +def maximize_channels(old_pixel: tuple, value: int) -> tuple: + """Return a new pixel that has colour channels set to the larger of value or the corresponding + colour channel in old_pixel. + + >>> example_pixel = (100, 12, 155) + >>> maximize_channels(example_pixel, 128) + (128, 128, 155) + """ + + +def divide_channels(old_pixel: tuple, denominator: int) -> tuple: + """Return a new pixel that has colour channels set to the quotient from dividing the + corresponding colour channel in old_pixel by denominator. + + >>> example_pixel = (100, 12, 155) + >>> divide_channels(example_pixel, 2) + (50, 6, 77) + """ + + +def add_pepper(pixel_data: list, k: int) -> list: + """Return a new list of pixels formed from the corresponding pixels in pixel_data that has some + randomly chosen black pixels. + + The chance that a new pixel will be black is based on k. The probability that a pixel is + black is to be : 1 / (k + 1) + + Assume that k >= 0. + + You must use the divide_channels function (above). + + Hint: use the function random.choice to choose from a list of denominators. What denominator, + passed to divide_channels, causes a pixel to not change its colour? What denominator causes a + pixel to become black? + + Because of the randomness, we can't specify an exact doctest. + """ + + +def add_salt(pixel_data: list, k: int) -> list: + """Return a new list of pixels formed from the corresponding pixels in pixel_data that has some + randomly chosen white pixels. + + The chance that a new pixel will be white is based on k. The probability that a pixel is + white is to be : 1 / (k + 1) + + Assume that k >= 0. + + You must use the maximize_channels function (above). + + Hint: use the function random.choice to choose from a list of values. What value, passed to + maximize_channels, causes a pixel to not change its colour? What value causes a pixel to become + white? + + Because of the randomness, we can't specify an exact doctest. + """ + + +def add_salt_and_pepper(pixel_data: list, k: int) -> list: + """Return a new list of pixels formed from the corresponding pixels in pixel_data + that has some randomly chosen white pixels and some randomly chosen black pixels. + + You must use the add_pepper then add_salt functions, in that order, with both using noise + probabilities determined by k. + + Because of the randomness, we can't specify an exact doctest. + """ + + +def run_salt_and_pepper_example(source: str, destination: str, k: int) -> None: + """Add salt and pepper noise to an example image file at source and save the result in + destination, where noisiness is a function of k. + + You can run this function with an image of your choice. Make sure the image is not too large or + this will take a long time. + """ + original_pixel_data, width, height = a1_image.load_image(source) + noisy_pixel_data = add_salt_and_pepper(original_pixel_data, k) + a1_image.save_image(destination, noisy_pixel_data, width, height) + + +if __name__ == '__main__': + import doctest + doctest.testmod() + + # When you are ready to check your work with python_ta, uncomment the following lines. + # (Delete the "#" and space before each line.) + # import python_ta + # python_ta.check_all(config={ + # 'extra-imports': ['random', 'a1_image'], + # 'max-line-length': 100 + # }) diff --git a/assignments/a1/a1_part5.py b/assignments/a1/a1_part5.py new file mode 100755 index 0000000..241bbcc --- /dev/null +++ b/assignments/a1/a1_part5.py @@ -0,0 +1,148 @@ +"""CSC110 Fall 2021 Assignment 1, Part 5 + +Instructions (READ THIS FIRST!) +=============================== + +Please follow the instructions in the assignment handout to complete this file. + +Throughout this module, we assume that images are at least 4 pixels wide and 4 pixels high. + +Copyright and Usage Information +=============================== + +This file is provided solely for the personal and private use of students +taking CSC110 at the University of Toronto St. George campus. All forms of +distribution of this code, whether as given or with any changes, are +expressly prohibited. For more information on copyright for CSC110 materials, +please consult our Course Syllabus. + +This file is Copyright (c) 2021 Mario Badr and Tom Fairgrieve. +""" +from statistics import median + +import a1_image + + +def create_example_pixel_data() -> list: + """Return a new list of pixels that can be used in the doctest examples. The list describes + an image that is 4 pixels wide and 4 pixels high. Each element in the list is a three-element + tuple that corresponds to the red, green, and blue colour channels, respectively. + + The pixels appear in the order of left-to-right, bottom-to-top (i.e., the same as + a1_image.load_image). + """ + return [(128, 128, 128), (35, 50, 65), (210, 32, 68), (32, 208, 43), # y = 0 (bottom) + (130, 20, 42), (43, 44, 45), (17, 243, 82), (61, 85, 92), # y = 1 + (201, 23, 23), (23, 23, 23), (42, 180, 19), (16, 58, 29), # y = 2 + (1, 52, 128), (26, 123, 128), (71, 234, 82), (23, 108, 34)] # y = 3 (top) + + +def get_pixel(pixel_data: list, image_width: int, x: int, y: int) -> tuple: + """Return a new RGB-tuple of the pixel at location (x, y) from the pixels in pixel_data that has + width image_width. + + Assume that pixel_data was obtained from an image using a1_image.load_image. + + >>> example_pixels = create_example_pixel_data() + >>> get_pixel(example_pixels, 4, 0, 0) + (128, 128, 128) + >>> get_pixel(example_pixels, 4, 1, 2) + (23, 23, 23) + """ + index = x + y * image_width + r, g, b = pixel_data[index] + + return r, g, b + + +def get_pixel_window(pixel_data: list, width: int, x: int, y: int) -> list: + """Return a new list of RGB-tuples of the pixel at (x, y), along with its neighbours, from the + pixels in pixel_data that has width image_width. + + A neighbouring pixel is a pixel that "touches" the pixel at (x, y), including diagonals. + + Assume that the location (x, y) is not on any edge of the image described in pixel_data. + + >>> example_pixels = create_example_pixel_data() + >>> get_pixel_window(example_pixels, 4, 1, 1) + [(201, 23, 23), (23, 23, 23), (42, 180, 19), (130, 20, 42), (43, 44, 45), (17, 243, 82), \ +(128, 128, 128), (35, 50, 65), (210, 32, 68)] + >>> get_pixel_window(example_pixels, 4, 2, 2) + [(26, 123, 128), (71, 234, 82), (23, 108, 34), (23, 23, 23), (42, 180, 19), (16, 58, 29), \ +(43, 44, 45), (17, 243, 82), (61, 85, 92)] + """ + return [get_pixel(pixel_data, width, x + j, y + i) for i in range(1, -1 - 1, -1) + for j in range(-1, 1 + 1)] + + +def separate_colour_channels(rgb_pixels: list) -> dict: + """Return a dictionary mapping the strings 'r', 'g', and 'b' to a list of the red, green, and + blue colour channels, respectively, in rgb_pixels. The value lists should have the same order + as rgb_pixels. + + >>> example_pixels = create_example_pixel_data() + >>> separate_colour_channels(example_pixels) == \ + {'r': [128, 35, 210, 32, 130, 43, 17, 61, 201, 23, 42, 16, 1, 26, 71, 23],\ + 'g': [128, 50, 32, 208, 20, 44, 243, 85, 23, 23, 180, 58, 52, 123, 234, 108],\ + 'b': [128, 65, 68, 43, 42, 45, 82, 92, 23, 23, 19, 29, 128, 128, 82, 34]} + True + """ + + +def calculate_median_colour(colour_channels: dict) -> tuple: + """Return the median colour in colour_channels. + + The median colour is the median value of each colour channel, type converted to an integer. + + Hint: use median from the statistics module. Be careful because the call to median may return a + float. + + >>> example_pixels = create_example_pixel_data() + >>> separated_colour_channels = separate_colour_channels(example_pixels) + >>> calculate_median_colour(separated_colour_channels) + (38, 71, 55) + """ + + +def apply_median_filter(pixel_data: list, image_width: int, image_height: int) -> list: + """Return a new list of pixels formed from the corresponding pixels in pixel_data (that + represents an image with width image_width and height image_height), except with a median filter + applied to the pixels. + + The median filter should not process boundaries (i.e., the edges of the image), so the returned + new list of pixels represents an image with width image_width - 2 and height image_height - 2. + + >>> example_pixels = create_example_pixel_data() + >>> apply_median_filter(example_pixels, 4, 4) + [(43, 44, 45), (35, 58, 45), (42, 52, 45), (26, 108, 45)] + """ + windows = [get_pixel_window(pixel_data, image_width, x, y) + for y in range(1, image_height - 1) for x in range(1, image_width - 1)] + window_channels = [separate_colour_channels(window) for window in windows] + return [calculate_median_colour(window_channel) for window_channel in window_channels] + + +def run_median_filter_example(source: str, destination: str) -> None: + """Apply the median filter to an example image file at source and save the result in + destination. + """ + original_pixel_data, original_width, original_height = a1_image.load_image(source) + + new_pixel_data = apply_median_filter(original_pixel_data, original_width, original_height) + + new_width = original_width - 2 + new_height = original_height - 2 + a1_image.save_image(destination, new_pixel_data, new_width, new_height) + + +if __name__ == '__main__': + import doctest + doctest.testmod() + + # When you are ready to check your work with python_ta, uncomment the following lines. + # (Delete the "#" and space before each line.) + import python_ta + python_ta.check_all(config={ + 'extra-imports': ['statistics', 'a1_image'], + 'max-line-length': 100 + }) diff --git a/assignments/a1/images/horses.png b/assignments/a1/images/horses.png new file mode 100755 index 0000000..4967e75 Binary files /dev/null and b/assignments/a1/images/horses.png differ diff --git a/assignments/a1/images/references.md b/assignments/a1/images/references.md new file mode 100755 index 0000000..3b20462 --- /dev/null +++ b/assignments/a1/images/references.md @@ -0,0 +1,5 @@ +# References + +Links to where the images came from. + +1. [horses.png](https://i2.pickpik.com/photos/83/964/942/horses-embracing-affectionate-equestrian-thumb.jpg) diff --git a/practice/prep2.py b/practice/prep2.py index 07ae930..5ed3465 100755 --- a/practice/prep2.py +++ b/practice/prep2.py @@ -14,7 +14,7 @@ In some function descriptions, we have written "You may ASSUME..." This means th when you are writing each function body, you only have to consider possible values for the parameters that satisfy these assumptions. -We have marked each place you need to write a doctest/code with the word +We have marked each place you need to write a doctest/code with the word As you complete your work in this file, delete each TO-DO comment---this is a good habit to get into early! To check your work, you should run this file in the Python console and then call each function manually (you can also copy-and-paste) diff --git a/requirements.txt b/requirements.txt index ee78317..5eb5ebe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ python-ta plotly pygame -requests~=2.25.1 +requests