#!/usr/bin/env python3 import sys import math import base64 import tkinter from io import BytesIO from PIL import Image as PILImage ## NO ADDITIONAL IMPORTS ALLOWED! def blurbox(n): kernel = [[1/n**2 for x in range(n)] for y in range(n)] return kernel class Image: def __init__(self, width, height, pixels): self.width = width self.height = height self.pixels = pixels def get_pixel(self, x, y): if x < 0: #edge effects for width x = 0 elif x >= self.width - 1: x = self.width - 1 if y < 0: #edge effects for height y = 0 elif y >= self.height - 1: y = self.height - 1 return self.pixels[(x + y * self.width)] #the index of the pixel in the pixel list def set_pixel(self, x, y, c): self.pixels[(x + y * self.width)] = c def apply_per_pixel(self, func): result = Image.new(self.width, self.height) for x in range(result.width): for y in range(result.height): color = self.get_pixel(x, y) newcolor = func(color) result.set_pixel(x, y, newcolor) #reversed order of x, y return result def inverted(self): return self.apply_per_pixel(lambda c: 255-c) #inverted + original = 255 def correlation(self, kernel): n = len(kernel) # n * n kernel 2D Array, and n is an odd integer result = Image.new(self.width, self.height) for x in range(result.width): for y in range(result.height): colornew = 0 #calculating the new color for every pixel for i in range(n): for j in range(n): # i, j are the size of the kernel colornew += self.get_pixel((x-(n//2)+j),(y-(n//2)+i)) * kernel[i][j] #calculating the correlation using the formula; n//2 is the "radius" of the 2D array result.set_pixel(x, y, colornew) #changing the color for every pixel return result def finalize(self): #final step is to clip the image and to round to integers for x in range(self.width): for y in range(self.height): current = self.get_pixel(x, y) if current < 0: #clipping to > 0 current = 0 elif current > 255: #clipping to < 255 current = 255 current = int(round(current)) #rounding to the closest integer self.set_pixel(x, y, current) def blurred(self, n): result = Image.new(self.width, self.height) blurrybox = blurbox(n) #generating a blurbox kernel with the size of n * n result = self.correlation(blurrybox) result.finalize() return result def sharpened(self, n): blurry = blurbox(n) identity = [[0 for x in range(n)] for y in range(n)] identity[n//2][n//2] = 2 #creating the 2 * I kernel combined = [[0 for x in range(n)] for y in range(n)] #initiate an empty n * n kernel for i in range(n): for j in range(n): combined[i][j]= identity[i][j] - blurry[i][j] #setting up a kernel that combines the (2 * I - B) result = self.correlation(combined) result.finalize() return result def edges(self): result = Image.new(self.width, self.height) K_x = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]] K_y = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]] O_x = self.correlation(K_x) O_y = self.correlation(K_y) #applying the 2 correlation kernels on the image and save them separately for x in range(self.width): for y in range(self.height): c = math.sqrt(O_x.get_pixel(x, y) ** 2 + O_y.get_pixel(x, y) ** 2) #calculate the resulting value using the formula result.set_pixel(x, y, c) result.finalize() return result def lowest_energy(self): #find the column number that has the lowest energy result = self.edges() #getting the edge map energy_map = [0 for x in range(self.width)] #a list that contains the sum of every column's energy for x in range(self.width): for y in range(self.height): energy_map[x] += result.get_pixel(x, y) #summing up the energy for every column x_lowest = energy_map.index(min(energy_map)) #find the index of the min return x_lowest def deletion(self): x_low = self.lowest_energy() result = Image.new((self.width - 1), (self.height)) #the resulting image will havge 1 fewer column for x in range(result.width): for y in range(result.height): if x < x_low: # if the column is to the left of the x_low, then we copy the pixels to the resulting image c = self.get_pixel(x, y) elif x >= x_low: # if the column is to the right of the x_low, then we copy the pixels that are 1 column right from the x_low (i.e ignoring the x_low column) c = self.get_pixel((x + 1), y) result.set_pixel(x, y, c) return result def retarget(self, columns): result = Image.new((self.width), (self.height)) result.pixels = self.pixels for i in range(columns): result = result.deletion() #repeating deletion and finding the lowest energy column for "the number of deleting columns times. return result # Below this point are utilities for loading, saving, and displaying # images, as well as for testing. def __eq__(self, other): return all(getattr(self, i) == getattr(other, i) for i in ('height', 'width', 'pixels')) @classmethod def load(cls, fname): """ Loads an image from the given file and returns an instance of this class representing that image. This also performs conversion to grayscale. Invoked as, for example: i = Image.load('test_images/cat.png') """ with open(fname, 'rb') as img_handle: img = PILImage.open(img_handle) img_data = img.getdata() if img.mode.startswith('RGB'): pixels = [round(.299*p[0] + .587*p[1] + .114*p[2]) for p in img_data] elif img.mode == 'LA': pixels = [p[0] for p in img_data] elif img.mode == 'L': pixels = list(img_data) else: raise ValueError('Unsupported image mode: %r' % img.mode) w, h = img.size return cls(w, h, pixels) @classmethod def new(cls, width, height): """ Creates a new blank image (all 0's) of the given height and width. Invoked as, for example: i = Image.new(640, 480) """ return cls(width, height, [0 for i in range(width*height)]) def save(self, fname, mode='PNG'): """ Saves the given image to disk or to a file-like object. If fname is given as a string, the file type will be inferred from the given name. If fname is given as a file-like object, the file type will be determined by the 'mode' parameter. """ out = PILImage.new(mode='L', size=(self.width, self.height)) out.putdata(self.pixels) if isinstance(fname, str): out.save(fname) else: out.save(fname, mode) out.close() def gif_data(self): """ Returns a base 64 encoded string containing the given image as a GIF image. Utility function to make show_image a little cleaner. """ buff = BytesIO() self.save(buff, mode='GIF') return base64.b64encode(buff.getvalue()) def show(self): """ Shows the given image in a new Tk window. """ global WINDOWS_OPENED if tk_root is None: # if tk hasn't been properly initialized, don't try to do anything. return WINDOWS_OPENED = True toplevel = tkinter.Toplevel() # highlightthickness=0 is a hack to prevent the window's own resizing # from triggering another resize event (infinite resize loop). see # https://stackoverflow.com/questions/22838255/tkinter-canvas-resizing-automatically canvas = tkinter.Canvas(toplevel, height=self.height, width=self.width, highlightthickness=0) canvas.pack() canvas.img = tkinter.PhotoImage(data=self.gif_data()) canvas.create_image(0, 0, image=canvas.img, anchor=tkinter.NW) def on_resize(event): # handle resizing the image when the window is resized # the procedure is: # * convert to a PIL image # * resize that image # * grab the base64-encoded GIF data from the resized image # * put that in a tkinter label # * show that image on the canvas new_img = PILImage.new(mode='L', size=(self.width, self.height)) new_img.putdata(self.pixels) new_img = new_img.resize((event.width, event.height), PILImage.NEAREST) buff = BytesIO() new_img.save(buff, 'GIF') canvas.img = tkinter.PhotoImage(data=base64.b64encode(buff.getvalue())) canvas.configure(height=event.height, width=event.width) canvas.create_image(0, 0, image=canvas.img, anchor=tkinter.NW) # finally, bind that function so that it is called when the window is # resized. canvas.bind('', on_resize) toplevel.bind('', lambda e: canvas.configure(height=e.height, width=e.width)) try: tk_root = tkinter.Tk() tk_root.withdraw() tcl = tkinter.Tcl() def reafter(): tcl.after(500,reafter) tcl.after(500,reafter) except: tk_root = None WINDOWS_OPENED = False if __name__ == '__main__': # code in this block will only be run when you explicitly run your script, # and not when the tests are being run. this is a good place for # generating images, etc. pass # the following code will cause windows from Image.show to be displayed # properly, whether we're running interactively or not: if WINDOWS_OPENED and not sys.flags.interactive: tk_root.mainloop() #w, h = 3, 3 #kernel1 = [[0 for x in range(w)] for y in range(h)] #kernel = [[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]] #kernel1 = [[0, 0.2, 0], [0.2, 0.2, 0.2], [0, 0.2, 0]] #i = Image.load('/Users/CatherineYang/Desktop/lab1/test_images/pigbird.png') #result = i.correlation(kernel) #result.finalize() #result.save('pigbird.PNG') #i = Image.load('/Users/CatherineYang/Desktop/lab1/test_images/cat.png') #result = i.blurred(5) #result.save('catblur.PNG') #i = Image.load('/Users/CatherineYang/Desktop/lab1/test_images/python.png') #result = i.sharpened(11) #result.save('pythonsharp.PNG') ##i = Image.load('/Users/CatherineYang/Desktop/lab1/test_images/construct.png') #result = i.edges() #result.save('constructedge.PNG') #i = Image.load('/Users/CatherineYang/Desktop/lab1/test_images/pigbird.png') #result = i.retarget(100) #result.save('pigbird100.PNG')