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/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/runConfigurations/Run_project.xml b/.idea/runConfigurations/Run_project.xml new file mode 100644 index 0000000..7b2c8e5 --- /dev/null +++ b/.idea/runConfigurations/Run_project.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index c26b869..8db849e 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,7 +1,13 @@ + + - + + + + - + + + { + "associatedIndex": 7 +} + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 32ca216..59e6eaf 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ pillow numpy ## run -python main.py +python src/Main.py ## menu items * file diff --git a/docs/UML/Class Diagram.puml b/docs/UML/Class Diagram.puml new file mode 100644 index 0000000..ccce635 --- /dev/null +++ b/docs/UML/Class Diagram.puml @@ -0,0 +1,60 @@ +@startuml + +class GUI{ + This is a singleton + --- + - ImageContainer _currentImage + - ImageManipulation[] _imageManipulations + + none initialise() +} + + +class ImageContainer{ + - imagedata _imageData + + none loadImage(path: str) + + imagedata getImage() + + pair getDimensions() +} + +abstract class ImageManipulation{ + +none manipulateImage(ImageContainer, Parameters) + +str[] getParameters() + +str getManipulationName() +} + +class CropImage{ +- str _name +} + +class GreyScaleImage{ +- str _name +} + +class SmoothImage{ +- str _name +} + +class RotateImage{ +- str _name +} + +class HSVImage{ +- str _name +} + +class HueShiftImage{ +- str _name +} + +GUI ..> ImageContainer +GUI ..> ImageManipulation + +CropImage ..|> ImageManipulation +GreyScaleImage ..|> ImageManipulation +SmoothImage ..|> ImageManipulation +RotateImage ..|> ImageManipulation +HSVImage ..|> ImageManipulation +HueShiftImage ..|> ImageManipulation + + +@enduml \ No newline at end of file diff --git a/docs/UML/Component Diagram.puml b/docs/UML/Component Diagram.puml new file mode 100644 index 0000000..2dfe922 --- /dev/null +++ b/docs/UML/Component Diagram.puml @@ -0,0 +1,7 @@ +@startuml +[GUI] +[Image Processing] + +[GUI] ..> [Image Processing] + +@enduml \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0e0efb4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pillow~=11.3.0 +numpy~=2.3.1 diff --git a/src/GUI.py b/src/GUI.py new file mode 100644 index 0000000..20718a4 --- /dev/null +++ b/src/GUI.py @@ -0,0 +1,75 @@ +import tkinter as tk +from tkinter import filedialog +from PIL import ImageTk +from ImageContainer import ImageContainer +from ImageManipulation.ManipulationList import * + + +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="Filters", menu=test_menu) + for manipulation in GetImageManipulationList(): + test_menu.add_command(label=manipulation.getManipulationName(), command=lambda: manipulation.manipulateImage()) + + # 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/CropImage.py b/src/ImageManipulation/CropImage.py new file mode 100644 index 0000000..2f63014 --- /dev/null +++ b/src/ImageManipulation/CropImage.py @@ -0,0 +1,39 @@ +from .ImageManipulation import ImageManipulation +from ..ImageContainer import ImageContainer +from typing import Any, List + + +class CropImage(ImageManipulation): + """Concrete implementation for cropping images.""" + + def __init__(self): + # You can initialize any instance variables here + self.manipulation_name = "Crop" + + 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 (e.g., crop coordinates) + """ + pass + + def getParameters(self) -> List[str]: + """ + Get the list of parameters required for cropping. + + Returns: + List of parameter names + """ + return ["width", "height"] + + def getManipulationName(self) -> str: + """ + Get the name of this manipulation operation. + + Returns: + The name of the manipulation + """ + return "Crop Image" \ 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/ImageManipulation/ManipulationList.py b/src/ImageManipulation/ManipulationList.py new file mode 100644 index 0000000..aeff678 --- /dev/null +++ b/src/ImageManipulation/ManipulationList.py @@ -0,0 +1,4 @@ +from src.ImageManipulation.CropImage import * + +def GetImageManipulationList() -> list: + return [CropImage()] \ No newline at end of file diff --git a/src/Main.py b/src/Main.py new file mode 100644 index 0000000..d79ee9f --- /dev/null +++ b/src/Main.py @@ -0,0 +1,6 @@ +#!/usr/bin/python +from GUI import GUI + +if __name__ == "__main__": + gui = GUI() + gui.initialise() \ No newline at end of file