diff --git a/.gitignore b/.gitignore index abd5980..98da2e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,179 @@ cache/* cache/** + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/docs/UML/Class Diagram.puml b/docs/UML/Class Diagram.puml index bd86e3a..67eca8f 100644 --- a/docs/UML/Class Diagram.puml +++ b/docs/UML/Class Diagram.puml @@ -5,45 +5,45 @@ class GUI{ --- - Image currentImage - ImageManipulation[] imageManipulations - + void initialise() + + none initialise() } class Image{ - imagedata image - + void setImage(imagedata) + + none loadImage(path: str) + imagedata getImage() + pair getDimensions() } abstract class ImageManipulation{ - +void manipulateImage(Image, Parameters) - +string[] getParameters() - +string getManipulationName() + +none manipulateImage(Image, Parameters) + +str[] getParameters() + +str getManipulationName() } class CropImage{ -- string name +- str name } class GreyScaleImage{ -- string name +- str name } class SmoothImage{ -- string name +- str name } class RotateImage{ -- string name +- str name } class HSVImage{ -- string +- str } class HueShiftImage{ -- string +- str } GUI ..> Image diff --git a/src/GUI.py b/src/GUI.py new file mode 100644 index 0000000..e15cb09 --- /dev/null +++ b/src/GUI.py @@ -0,0 +1,117 @@ +import tkinter as tk +from tkinter import filedialog +from PIL import ImageTk +from ImageContainer import ImageContainer + + +class GUI: + """The GUI class responsible for the main application GUI. + + @warning This class is a singleton. + """ + _instance = None + + # If the GUI has been initialised. + _isInitialised = False + + _currentImage = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(GUI, cls).__new__(cls) + return cls._instance + + def initialise(self): + """Initialise the GUI.""" + + if self._isInitialised: + return + else: + self._isInitialised = True + + # Main window + root = tk.Tk() + root.title("Image Viewer") + root.geometry("800x600") + root.config(bg="white") + icon = tk.PhotoImage(file='icon.png') + root.tk.call('wm', 'iconphoto', root._w, icon) + root.minsize(800, 600) + + # Menus + menu = tk.Menu(root) + root.config(menu=menu) + file_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Open Image", command=lambda: self._openImage()) + file_menu.add_command(label="Save Image", command=lambda: _save_image(OPENED_IMAGE)) + file_menu.add_command(label="Exit", command=root.quit) + + test_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="Test", menu=test_menu) + test_menu.add_command(label="Padding", command=lambda: padding(OPENED_IMAGE, + border_width=50) if OPENED_IMAGE is not None else print( + "No image opened.")) + test_menu.add_command(label="Crop", command=lambda: crop(OPENED_IMAGE) if OPENED_IMAGE is not None else print( + "No image opened.")) + test_menu.add_command(label="Resize", + command=lambda: resize(OPENED_IMAGE) if OPENED_IMAGE is not None else print( + "No image opened.")) + test_menu.add_command(label="Copy", command=lambda: copy(OPENED_IMAGE) if OPENED_IMAGE is not None else print( + "No image opened.")) + test_menu.add_command(label="Greyscale", + command=lambda: grayscale(OPENED_IMAGE) if OPENED_IMAGE is not None else print( + "No image opened.")) + test_menu.add_command(label="Hsv", command=lambda: hsv(OPENED_IMAGE) if OPENED_IMAGE is not None else print( + "No image opened.")) + test_menu.add_command(label="Hue Shifted", + command=lambda: hue_shifted(OPENED_IMAGE, hue=50) if OPENED_IMAGE is not None else print( + "No image opened.")) + test_menu.add_command(label="Smoothed", + command=lambda: smoothing(OPENED_IMAGE) if OPENED_IMAGE is not None else print( + "No image opened.")) + test_menu.add_command(label="Rotated", command=lambda: rotation(OPENED_IMAGE, + rotation_angle=90) if OPENED_IMAGE is not None else print( + "No image opened.")) + + clipboard_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="Clipboard", menu=clipboard_menu) + + image_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="Image", menu=image_menu) + + tools_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="Tools", menu=tools_menu) + + shapes_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="Shapes", menu=shapes_menu) + + colors_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="Colors", menu=colors_menu) + + layer_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="Layer", menu=layer_menu) + + filter_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="Filter", menu=filter_menu) + # + + # Frame to hold image + imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2) + imgframe.pack(side="top", pady=10) + + # Label to display image + image_label = tk.Label(imgframe, width=500, height=500, bg="white") + image_label.pack(expand=True) + + root.mainloop() + + def _openImage(self) -> None: + file_path = filedialog.askopenfilename( + filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")] + ) + if not file_path: + return + + self._currentImage = ImageContainer() + self._currentImage.loadImage(file_path) \ No newline at end of file diff --git a/src/ImageContainer.py b/src/ImageContainer.py new file mode 100644 index 0000000..4f948eb --- /dev/null +++ b/src/ImageContainer.py @@ -0,0 +1,25 @@ +import cv2 +from PIL import Image, ImageTk + +class ImageContainer: + _imageData = None + _path = None + + def loadImage(self, path: str) -> None: + """ Load image file from the path. + + :param path: Path to the image file. + """ + imgcv2 = cv2.imread(path) + height, width, channels = imgcv2.shape + + # Open and resize image (optional) + _imageData = Image.open(path) + _imageData = _imageData.resize((height, width), Image.LANCZOS) # Resize to fit frame + + _path = path + + print("Opened image:", path) + + def getDimensions(self) -> tuple[int,int]: + return self._imageData.shape \ No newline at end of file diff --git a/src/ImageManipulation/ImageManipulation.py b/src/ImageManipulation/ImageManipulation.py new file mode 100644 index 0000000..907796b --- /dev/null +++ b/src/ImageManipulation/ImageManipulation.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod +from typing import Any + +import sys + +sys.path.append('..') +from ..ImageContainer import ImageContainer + + +class ImageManipulation(ABC): + """Abstract base class for image manipulation operations.""" + + @abstractmethod + def manipulateImage(self, image: ImageContainer, parameters: Any) -> None: + """ + Manipulate the given image using the provided parameters. + + Args: + image: The image to manipulate + parameters: Parameters for the manipulation + """ + pass + + @abstractmethod + def getParameters(self) -> list[str]: + """ + Get the list of parameters required for this manipulation. + + Returns: + List of parameter names + """ + pass + + @abstractmethod + def getManipulationName(self) -> str: + """ + Get the name of this manipulation operation. + + Returns: + The name of the manipulation + """ + pass diff --git a/src/Main.py b/src/Main.py new file mode 100644 index 0000000..d9c6bbc --- /dev/null +++ b/src/Main.py @@ -0,0 +1,5 @@ +from GUI import GUI + +if __name__ == "__main__": + gui = GUI() + gui.initialise() \ No newline at end of file