diff --git a/GUI.py b/GUI.py index 6d6fa51b906ce7e0957886cb191a6c73d7a49f71..a48cdf92c6d267de75e66d1e03e2ef19adb76cfe 100644 --- a/GUI.py +++ b/GUI.py @@ -1,4 +1,4 @@ -from tkinter import Tk, Label, Button, StringVar, OptionMenu +from tkinter import StringVar, Label, Button, OptionMenu, Tk from tkinter.filedialog import askopenfilename from tokenize import String @@ -11,6 +11,10 @@ from filter import * class GUI(object): def __init__(self, master): + """ + This class defines the GUI as one. + :param master: The Tk root. + """ self.master = master self.master.title('Filter') @@ -50,7 +54,13 @@ class GUI(object): self.original_image_label.grid(row=1, column=0) self.filtered_image_label.grid(row=1, column=1) - def read_image(self, path: String): + def read_image(self, path: String) -> ImageTk.PhotoImage: + """ + This reads an image from a given path and resize it. + + :param path: The path to the image. + :return: A image to be used in the frontend + """ return ImageTk.PhotoImage( Image.open(path).resize( (self.image_label_width, self.image_label_height), @@ -58,15 +68,28 @@ class GUI(object): ) ) - def _update_label_image(self, label: Label, new_image, row: int, column: int): + def _update_label_image(self, label: Label, new_image, row: int, column: int) -> None: + """ + This method updates to label with a new image. + + :param label: The reference to the label which should be changes. + :param new_image: The reference to the new image. + :param row: Row in the GUI. + :param column: Column in the GUI. + :return: None + """ label.destroy() label = Label(self.master, image=new_image) label.image = new_image label.pack() label.grid(row=row, column=column) - def set_new_file(self): - # Todo: Specify more formats + def set_new_file(self) -> None: + """ + This sets the new file and refreshed the paths as well with the lables. + + :return: None + """ self.image_path: String = askopenfilename(initialdir='/', title='Select file', filetypes=(('jpeg files', '*.jpeg'), ('all files', '*.*'))) @@ -77,17 +100,39 @@ class GUI(object): self.filter.set(FILTER.empty.value) self._update_label_image(self.filtered_image_label, new_image, 1, 1) - def change_filter(self, *args): + def change_filter(self, *args) -> None: + """ + This changes the filter and updates the label for the edited images. + + :param args: Arguments of the listener. + :return: None + """ self.edit_image() edited_image = self.read_image(self.edit_path) self._update_label_image(self.filtered_image_label, edited_image, 1, 1) - def edit_image(self): + def edit_image(self) -> None: + """ + This method edits a given image and saves it. + + :return: None + """ print('From: ' + self.image_path + ' to ' + self.edit_path + ' with ' + self.filter.get()) Editor(self.image_path, self.edit_path).do_convolution_with(self.filter.get()) + print('Done...') if __name__ == '__main__': + """ + This program is for editing images with a given filter. + The user can choose between all filters defined in the FILTER enum. + + To use the program run: + $ python3 GUI.py + + Notice: The edited image will be saved in ./images/edited.py. Each new filter will overwrite this image. This may + take a while. + """ root = Tk() root.resizable(False, False) my_gui = GUI(root) diff --git a/color.py b/color.py deleted file mode 100644 index d940325aec3c7da36879a1b8ec63c5be5077071e..0000000000000000000000000000000000000000 --- a/color.py +++ /dev/null @@ -1,7 +0,0 @@ -from enum import Enum - - -class COLOR(Enum): - red = 0 - green = 1 - blue = 2 diff --git a/color_enum.py b/color_enum.py new file mode 100644 index 0000000000000000000000000000000000000000..b9e0e1e601484faf51ca21ed4de746721dd9d449 --- /dev/null +++ b/color_enum.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class COLOR(Enum): + """ + This enum defines the index-range of the colors in the image. + Notice: All images must be a RGB-image (MxNx3). + """ + red = 0 + green = 1 + blue = 2 diff --git a/editor.py b/editor.py index 8be8a9e9f3e5216e7de1f6bc8a6c1c452fa5953b..762bd6b5a03ccaa73c5d5573adbcf7910e7e8402 100644 --- a/editor.py +++ b/editor.py @@ -1,3 +1,6 @@ +import copy +from tokenize import String + from skimage import io from pixel import * @@ -5,14 +8,26 @@ from pixel import * class Editor(object): - def __init__(self, origin_path, edit_path): + def __init__(self, origin_path: String, edit_path: String): + """ + This class is the editor which edits a given image and stores a new one. + + :param origin_path: The original image which should be edited. + :param edit_path: The edited image which should be stored. + """ self.image_path = origin_path self.edit_path = edit_path - def do_convolution_with(self, option): + def do_convolution_with(self, option: String) -> None: + """ + This method iterates over the image and changes each pixel by a given method. + Notice, that the image must be a RGB-image (MxNx3). + + :param option: The options defined in FILTER. + :return: None + """ image = io.imread(fname=self.image_path) - edited_image = io.imread(fname=self.image_path) # copy.deepcopy(image) - # if image is grey scale then there is only width and height + edited_image = copy.deepcopy(image) # io.imread(fname=self.image_path) height, width, _ = image.shape for v in range(height): diff --git a/filter.py b/filter.py index 0c9ba5433bb0d9a6d5a9c779ac4faf9b1a4e633c..58ca9cfbab9a9f4f449d6191bb05a673cb430535 100644 --- a/filter.py +++ b/filter.py @@ -1,35 +1,55 @@ import numpy as np +from enum import Enum +from tokenize import String +from typing import List, Tuple +from unittest.test.testmock.testpatch import function -from color import * -from mask import * - +import imageio -class FILTER(Enum): - empty = '---' - mod_laplacian = 'Modified Laplacian' - laplacian = 'Laplacian' - gaussian = 'Gaussian' - median = 'Median' - box = 'Box' - min = 'Min' - max = 'Max' +from color_enum import * +from filter_enum import * +from mask import * class Filter(object): - def __init__(self, image, v, u): + def __init__(self, image: imageio.core.util.Array, v: int, u: int) -> None: + """ + This class edits a pixel in a image with a given filter. + + :param image: The image in which the filter should be used. + :param v: The v coordinate + :param u: The u coordinate + """ self.image = image self.v = v self.u = u - def use(self, option, linear=True, dim=(5, 5)): + def use(self, option: String = FILTER.empty.value, linear: bool = True, dim: Tuple = (5, 5), + f: function = np.min) -> List: + """ + With this method it can be decided if a linear or non-linear filter should be used. + + :param f: The filter function to be used. + :param option: The name of the filter to be used. + :param linear: Is the filter linear. + :param dim: The dimension of the filter. + :return: A new rgb-pixel + """ if linear: return self.linear_filter(option) else: - return self.non_linear_filter(option, dim) + return self.non_linear_filter(f, dim) + + def linear_filter(self, option: String) -> List: + """ + This method is for filtering with linear filters. + Notice: All filter-matrices must have a uneven dimension. + + :param option: The name of the filter to be used. + :return: A modified rgb-pixel + """ - def linear_filter(self, option): - # just filter with uneven dimensions height_I, width_I, _ = self.image.shape H = np.matrix(Mask().H[option]) @@ -50,9 +70,16 @@ class Filter(object): np.multiply(1 / divider, H[center_j + j, center_i + i]) return new_pixel - def non_linear_filter(self, option, dim=(5, 5)): + def non_linear_filter(self, f: function, dim: Tuple = (5, 5)) -> List: + """ + This method is for filtering with linear filters. + Notice: All filter-regions R must have a uneven dimension. + + :param f: The filter function to be used. + :param dim: The dimension of filter-region R. + :return: A modified rgb-pixel + """ - # just filter with uneven dimensions height_I, width_I, _ = self.image.shape height_R, width_R = dim center_i = center_j = int(height_R / 2) @@ -65,21 +92,8 @@ class Filter(object): for color in COLOR: colors_neighbors[color.value] += [self.image[self.v + j, self.u + i, color.value]] - if option == FILTER.median.value: - return [ - np.median(colors_neighbors[COLOR.red.value]), - np.median(colors_neighbors[COLOR.green.value]), - np.median(colors_neighbors[COLOR.blue.value]) - ] - elif option == FILTER.min.value: - return [ - np.min(colors_neighbors[COLOR.red.value]), - np.min(colors_neighbors[COLOR.green.value]), - np.min(colors_neighbors[COLOR.blue.value]) - ] - elif option == FILTER.max.value: - return [ - np.max(colors_neighbors[COLOR.red.value]), - np.max(colors_neighbors[COLOR.green.value]), - np.max(colors_neighbors[COLOR.blue.value]) - ] + return [ + f(colors_neighbors[COLOR.red.value]), + f(colors_neighbors[COLOR.green.value]), + f(colors_neighbors[COLOR.blue.value]) + ] diff --git a/filter_enum.py b/filter_enum.py new file mode 100644 index 0000000000000000000000000000000000000000..dc8c77e5ffbb7a121b012b6bd456d64c641a5728 --- /dev/null +++ b/filter_enum.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class FILTER(Enum): + """ + This enum defines all filters. + """ + empty = '---' + mod_laplacian = 'Modified Laplacian' + laplacian = 'Laplacian' + gaussian = 'Gaussian' + median = 'Median' + box = 'Box' + min = 'Min' + max = 'Max' diff --git a/images/edit.jpeg b/images/edit.jpeg index 8782bb9b020ec6fbecf2a5b2d9919486ec4e4739..60dc966838f446baf89a8d602d08a3b0f49ceb32 100644 Binary files a/images/edit.jpeg and b/images/edit.jpeg differ diff --git a/mask.py b/mask.py index 77cfbc1d18b07db71e9e83603267e14c8490f622..b1926a7c01fdc77fe9c6daafbbb7776ed965c488 100644 --- a/mask.py +++ b/mask.py @@ -1,9 +1,14 @@ +from typing import List + from filter import * class Mask(object): def __init__(self): + """ + This class contains all filter matrices which can be accessed via H. + """ self.H = { FILTER.gaussian.value: self._gaussian(), FILTER.laplacian.value: self._laplacian(), @@ -12,7 +17,11 @@ class Mask(object): } @staticmethod - def _gaussian(): + def _gaussian() -> List[List]: + """ + Gaussian-Blur for smoothing images. + :return: Gaussian-Blur-Matrix + """ return [[0, 1, 2, 1, 0], [1, 3, 5, 3, 1], [2, 5, 9, 5, 2], @@ -20,15 +29,24 @@ class Mask(object): [0, 1, 2, 1, 0]] @staticmethod - def _laplacian(): + def _laplacian() -> List[List]: + """ + Laplacian-Filter is a difference-filter for finding edges. + :return: Laplacian-Filter-Matrix + """ return [[0, 0, -1, 0, 0], [0, -1, -2, -1, 0], - [-1, -2, 10, -2, -1], + [-1, -2, 16, -2, -1], [0, -1, -2, -1, 0], [0, 0, -1, 0, 0]] @staticmethod - def _modified_laplacian(): + def _modified_laplacian() -> List[List]: + """ + Modified Laplacian-Filter is a difference-filter for finding edges. + It emphasizes the edges more. + :return: Modified-Laplacian-Filter-Matrix + """ return [[0, 0, -1, 0, 0], [0, -1, -200, -1, 0], [-1, -200, 1000, -200, -1], @@ -36,7 +54,11 @@ class Mask(object): [0, 0, -1, 0, 0]] @staticmethod - def _box(): + def _box() -> List[List]: + """ + Box-Blur for smoothing images. + :return: Box-Blur-Filter-Matrix + """ return [[0, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 1, 1, 1, 0], diff --git a/pixel.py b/pixel.py index d6bfe21c459754daaaf50254956d33ca9cca9ca5..97e585cae2c8a30cc50fa5ec56c712f4874d0ae4 100644 --- a/pixel.py +++ b/pixel.py @@ -1,13 +1,31 @@ -from filter import * +import numpy as np +from typing import List + +import imageio + +from filter import FILTER, Filter class Pixel(object): - def __init__(self, image, v, u): + def __init__(self, image: imageio.core.util.Array, v: int, u: int): + """ + This class edits a pixel in a image with a given filter. + + :param image: The image in which the filter should be used. + :param v: The v coordinate + :param u: The u coordinate + """ self.image = image self.v = v self.u = u - def filter_with(self, option: str): + def filter_with(self, option: str) -> List: + """ + This method gets the new pixel edited with a given filter. + + :param option: The name of the filter to be used. + :return: New pixel edited with a given filter + """ if option == FILTER.empty.value: return self.image[self.v, self.u] elif option == FILTER.laplacian.value: @@ -19,8 +37,8 @@ class Pixel(object): elif option == FILTER.box.value: return Filter(self.image, self.v, self.u).use(FILTER.box.value) elif option == FILTER.median.value: - return Filter(self.image, self.v, self.u).use(FILTER.median.value, linear=False, dim=(5, 5)) + return Filter(self.image, self.v, self.u).use(linear=False, dim=(5, 5), f=np.median) elif option == FILTER.min.value: - return Filter(self.image, self.v, self.u).use(FILTER.min.value, linear=False, dim=(5, 5)) + return Filter(self.image, self.v, self.u).use(linear=False, dim=(5, 5), f=np.min) elif option == FILTER.max.value: - return Filter(self.image, self.v, self.u).use(FILTER.max.value, linear=False, dim=(5, 5)) + return Filter(self.image, self.v, self.u).use(linear=False, dim=(5, 5), f=np.max) diff --git a/requirements.txt b/requirements.txt index 32ba7c44ffc8bed6578d56c33d4f9efee0272108..0f25081e9120a9155e068763ee1ee1990fe5c119 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +imageio==2.5.0 Pillow==6.0.0 scikit-image==0.15.0 numpy == 1.16.2 \ No newline at end of file