transferring functionalities from main.py to src
This commit is contained in:
221
src/GUI.py
221
src/GUI.py
@@ -1,75 +1,146 @@
|
||||
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)
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
from PIL import ImageTk
|
||||
from ImageContainer import ImageContainer
|
||||
from ImageManipulation.ManipulationList import *
|
||||
from functools import partial
|
||||
|
||||
|
||||
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: self._saveImage())
|
||||
file_menu.add_command(label="Exit", command=root.quit)
|
||||
|
||||
edit_menu = tk.Menu(menu, tearoff=0)
|
||||
menu.add_cascade(label="Edit", menu=edit_menu)
|
||||
edit_menu.add_command(label="Undo", accelerator="Ctrl+Z", command=lambda: self._undo())
|
||||
|
||||
|
||||
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=partial(self._applyManipulation, manipulation)
|
||||
)
|
||||
|
||||
# Manual Test menu listing all manipulations explicitly
|
||||
manual_menu = tk.Menu(menu, tearoff=0)
|
||||
menu.add_cascade(label="Test", menu=manual_menu)
|
||||
manual_menu.add_command(label="Padding", command=partial(self._applyManipulation, Padding(), {"border_width": 50}))
|
||||
manual_menu.add_command(label="Crop", command=partial(self._applyManipulation, CropImage()))
|
||||
manual_menu.add_command(label="Resize", command=partial(self._applyManipulation, ResizeImage(), {"width": 200, "height": 200}))
|
||||
manual_menu.add_command(label="Copy", command=partial(self._applyManipulation, CopyImage()))
|
||||
manual_menu.add_command(label="Greyscale", command=partial(self._applyManipulation, Grayscale()))
|
||||
manual_menu.add_command(label="HSV", command=partial(self._applyManipulation, HSV()))
|
||||
manual_menu.add_command(label="Hue Shifted", command=partial(self._applyManipulation, HueShift(), {"hue": 50}))
|
||||
manual_menu.add_command(label="Smoothed", command=partial(self._applyManipulation, BoxBlur(), {"ksize": 15}))
|
||||
manual_menu.add_command(label="Rotated", command=partial(self._applyManipulation, RotateImage(), {"angle": 90}))
|
||||
manual_menu.add_command(label="Flip (Horizontal)", command=partial(self._applyManipulation, FlipImage(), {"mode": "horizontal"}))
|
||||
manual_menu.add_command(label="Flip (Vertical)", command=partial(self._applyManipulation, FlipImage(), {"mode": "vertical"}))
|
||||
manual_menu.add_command(label="Color Adjust", command=partial(self._applyManipulation, ColorAdjust(), {"brightness": 10, "contrast": 1.2, "saturation": 1.1}))
|
||||
manual_menu.add_command(label="Gaussian Blur", command=partial(self._applyManipulation, GaussianBlur(), {"ksize": 5}))
|
||||
manual_menu.add_command(label="Sobel Edge", command=partial(self._applyManipulation, SobelEdge(), {"dx": 1, "dy": 0, "ksize": 3}))
|
||||
manual_menu.add_command(label="Binary Threshold", command=partial(self._applyManipulation, BinaryThreshold(), {"thresh": 127}))
|
||||
manual_menu.add_command(label="Histogram Threshold", command=partial(self._applyManipulation, HistogramThreshold()))
|
||||
|
||||
# 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
|
||||
self._imageLabel = tk.Label(imgframe, width=500, height=500, bg="white")
|
||||
self._imageLabel.pack(expand=True)
|
||||
|
||||
self._root = root
|
||||
|
||||
# Key bindings
|
||||
root.bind_all('<Control-z>', lambda event: self._undo())
|
||||
|
||||
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)
|
||||
self._renderCurrentImage()
|
||||
|
||||
def _renderCurrentImage(self) -> None:
|
||||
if self._currentImage is None or self._currentImage.getImage() is None:
|
||||
return
|
||||
pil_img = self._currentImage.getImage()
|
||||
width, height = pil_img.size
|
||||
tk_img = ImageTk.PhotoImage(pil_img)
|
||||
# Keep reference to avoid garbage collection
|
||||
self._imageLabel.image = tk_img
|
||||
self._imageLabel.config(image=tk_img, width=width, height=height)
|
||||
|
||||
def _applyManipulation(self, manipulation, params: dict | None = None) -> None:
|
||||
if self._currentImage is None:
|
||||
return
|
||||
# Take undo snapshot
|
||||
self._currentImage.snapshot()
|
||||
if params is None:
|
||||
# default demo params for crop
|
||||
params = {"width": 200, "height": 200}
|
||||
manipulation.manipulateImage(self._currentImage, params)
|
||||
self._renderCurrentImage()
|
||||
|
||||
def _saveImage(self) -> None:
|
||||
if self._currentImage is None or self._currentImage.getImage() is None:
|
||||
return
|
||||
path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[
|
||||
("PNG", "*.png"), ("JPEG", "*.jpg;*.jpeg"), ("Bitmap", "*.bmp"), ("All Files", "*.*")
|
||||
])
|
||||
if not path:
|
||||
return
|
||||
self._currentImage.saveImage(path)
|
||||
|
||||
def _undo(self) -> None:
|
||||
if self._currentImage is None:
|
||||
return
|
||||
self._currentImage.undo()
|
||||
self._renderCurrentImage()
|
||||
@@ -1,25 +1,51 @@
|
||||
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
|
||||
import cv2
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
class ImageContainer:
|
||||
_imageData = None
|
||||
_path = None
|
||||
_history = 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)
|
||||
self._imageData = Image.open(path)
|
||||
# PIL expects (width, height)
|
||||
self._imageData = self._imageData.resize((width, height), Image.LANCZOS)
|
||||
|
||||
self._path = path
|
||||
self._history = []
|
||||
|
||||
print("Opened image:", path)
|
||||
|
||||
def getDimensions(self) -> tuple[int,int]:
|
||||
# PIL Image.size returns (width, height)
|
||||
return self._imageData.size if self._imageData is not None else (0, 0)
|
||||
|
||||
def getImage(self) -> Image:
|
||||
return self._imageData
|
||||
|
||||
def saveImage(self, path: str) -> None:
|
||||
if self._imageData is not None:
|
||||
self._imageData.save(path)
|
||||
|
||||
def snapshot(self) -> None:
|
||||
"""Push a copy of current image to history for undo."""
|
||||
if self._imageData is None:
|
||||
return
|
||||
# Ensure a deep copy (PIL copy is sufficient)
|
||||
self._history.append(self._imageData.copy())
|
||||
# Cap history size to avoid memory blow-up
|
||||
if len(self._history) > 20:
|
||||
self._history.pop(0)
|
||||
|
||||
def undo(self) -> None:
|
||||
if not self._history:
|
||||
return
|
||||
self._imageData = self._history.pop()
|
||||
28
src/ImageManipulation/BoxBlur.py
Normal file
28
src/ImageManipulation/BoxBlur.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
|
||||
|
||||
class BoxBlur(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Box Blur"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["ksize"]
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
k = 15
|
||||
if isinstance(parameters, dict) and parameters.get("ksize"):
|
||||
k = int(parameters.get("ksize"))
|
||||
if k % 2 == 0:
|
||||
k += 1
|
||||
blurred = cv2.blur(cv_img, (k, k))
|
||||
image._imageData = cv2_to_pil(blurred)
|
||||
|
||||
|
||||
45
src/ImageManipulation/ColorAdjust.py
Normal file
45
src/ImageManipulation/ColorAdjust.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
class ColorAdjust(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Color Adjust"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["brightness", "contrast", "saturation"]
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
|
||||
brightness = 0.0
|
||||
contrast = 1.0
|
||||
saturation = 1.0
|
||||
|
||||
if isinstance(parameters, dict):
|
||||
if parameters.get("brightness") is not None:
|
||||
brightness = float(parameters.get("brightness")) # -100..100 (additive)
|
||||
if parameters.get("contrast") is not None:
|
||||
contrast = float(parameters.get("contrast")) # 0.0..3.0 (multiplicative)
|
||||
if parameters.get("saturation") is not None:
|
||||
saturation = float(parameters.get("saturation")) # 0.0..3.0 (multiplicative)
|
||||
|
||||
# Apply brightness/contrast on BGR
|
||||
adjusted = cv2.convertScaleAbs(cv_img, alpha=contrast, beta=brightness)
|
||||
|
||||
# Adjust saturation in HSV
|
||||
hsv = cv2.cvtColor(adjusted, cv2.COLOR_BGR2HSV).astype(np.float32)
|
||||
h, s, v = cv2.split(hsv)
|
||||
s = np.clip(s * saturation, 0, 255)
|
||||
hsv = cv2.merge([h, s, v]).astype(np.uint8)
|
||||
result = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
||||
|
||||
image._imageData = cv2_to_pil(result)
|
||||
|
||||
23
src/ImageManipulation/CopyImage.py
Normal file
23
src/ImageManipulation/CopyImage.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import numpy as np
|
||||
|
||||
|
||||
class CopyImage(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Copy"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
# Deep copy using numpy array roundtrip to ensure a new buffer
|
||||
pil_img = image.getImage()
|
||||
arr = np.array(pil_img)
|
||||
image._imageData = pil_img.copy()
|
||||
|
||||
|
||||
@@ -1,39 +1,69 @@
|
||||
from ImageManipulation.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
|
||||
"""
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from PIL import Image
|
||||
|
||||
|
||||
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)
|
||||
"""
|
||||
if image is None or getattr(image, "_imageData", None) is None:
|
||||
return
|
||||
|
||||
pil_image = image._imageData
|
||||
|
||||
# If parameters provided as dict with width/height, do centered crop
|
||||
if isinstance(parameters, dict) and "width" in parameters and "height" in parameters:
|
||||
target_width = int(parameters["width"]) if parameters["width"] is not None else None
|
||||
target_height = int(parameters["height"]) if parameters["height"] is not None else None
|
||||
if target_width is None or target_height is None:
|
||||
return
|
||||
img_width, img_height = pil_image.size
|
||||
crop_width = min(target_width, img_width)
|
||||
crop_height = min(target_height, img_height)
|
||||
left = (img_width - crop_width) // 2
|
||||
top = (img_height - crop_height) // 2
|
||||
right = left + crop_width
|
||||
bottom = top + crop_height
|
||||
else:
|
||||
# Default: trim margins similar to demo in root main.py
|
||||
img_width, img_height = pil_image.size
|
||||
left = 80
|
||||
top = 80
|
||||
right = max(0, img_width - 130)
|
||||
bottom = max(0, img_height - 130)
|
||||
if right <= left or bottom <= top:
|
||||
return
|
||||
|
||||
cropped = pil_image.crop((left, top, right, bottom))
|
||||
image._imageData = cropped
|
||||
|
||||
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"
|
||||
84
src/ImageManipulation/Filters.py
Normal file
84
src/ImageManipulation/Filters.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
|
||||
|
||||
class GaussianBlur(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Gaussian Blur"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["ksize"] # odd int
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
ksize = 5
|
||||
if isinstance(parameters, dict) and parameters.get("ksize"):
|
||||
ksize = int(parameters.get("ksize"))
|
||||
if ksize % 2 == 0:
|
||||
ksize += 1
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
blurred = cv2.GaussianBlur(cv_img, (ksize, ksize), 0)
|
||||
image._imageData = cv2_to_pil(blurred)
|
||||
|
||||
|
||||
class SobelEdge(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Sobel Edge"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["dx", "dy", "ksize"]
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)
|
||||
dx = int(parameters.get("dx", 1)) if isinstance(parameters, dict) else 1
|
||||
dy = int(parameters.get("dy", 0)) if isinstance(parameters, dict) else 0
|
||||
ksize = int(parameters.get("ksize", 3)) if isinstance(parameters, dict) else 3
|
||||
sobel = cv2.Sobel(gray, cv2.CV_64F, dx, dy, ksize=ksize)
|
||||
abs_sobel = cv2.convertScaleAbs(sobel)
|
||||
image._imageData = cv2_to_pil(abs_sobel)
|
||||
|
||||
|
||||
class BinaryThreshold(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Binary Threshold"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["thresh"]
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)
|
||||
thresh_val = int(parameters.get("thresh", 127)) if isinstance(parameters, dict) else 127
|
||||
_, thresh = cv2.threshold(gray, thresh_val, 255, cv2.THRESH_BINARY)
|
||||
image._imageData = cv2_to_pil(thresh)
|
||||
|
||||
|
||||
class HistogramThreshold(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Histogram Threshold"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)
|
||||
# Otsu's threshold as histogram-based method
|
||||
_, otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||
image._imageData = cv2_to_pil(otsu)
|
||||
|
||||
26
src/ImageManipulation/FlipImage.py
Normal file
26
src/ImageManipulation/FlipImage.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
|
||||
|
||||
class FlipImage(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Flip"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["mode"] # horizontal|vertical
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
mode = "horizontal"
|
||||
if isinstance(parameters, dict) and parameters.get("mode") in ("horizontal", "vertical"):
|
||||
mode = parameters.get("mode")
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
flip_code = 1 if mode == "horizontal" else 0
|
||||
flipped = cv2.flip(cv_img, flip_code)
|
||||
image._imageData = cv2_to_pil(flipped)
|
||||
|
||||
23
src/ImageManipulation/Grayscale.py
Normal file
23
src/ImageManipulation/Grayscale.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
|
||||
|
||||
class Grayscale(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Grayscale"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)
|
||||
image._imageData = cv2_to_pil(gray)
|
||||
|
||||
|
||||
23
src/ImageManipulation/HSV.py
Normal file
23
src/ImageManipulation/HSV.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
|
||||
|
||||
class HSV(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "HSV"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
hsv = cv2.cvtColor(cv_img, cv2.COLOR_BGR2HSV)
|
||||
image._imageData = cv2_to_pil(hsv)
|
||||
|
||||
|
||||
32
src/ImageManipulation/HueShift.py
Normal file
32
src/ImageManipulation/HueShift.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
class HueShift(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Hue Shift"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["hue"]
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
hsv = cv2.cvtColor(cv_img, cv2.COLOR_BGR2HSV).astype(np.uint8)
|
||||
hue_delta = 50
|
||||
if isinstance(parameters, dict) and parameters.get("hue") is not None:
|
||||
hue_delta = int(parameters.get("hue"))
|
||||
h, s, v = cv2.split(hsv)
|
||||
# OpenCV H range is [0,179]; wrap around using modulo
|
||||
h = ((h.astype(np.int16) + hue_delta) % 180).astype(np.uint8)
|
||||
hsv_shifted = cv2.merge([h, s, v])
|
||||
bgr = cv2.cvtColor(hsv_shifted, cv2.COLOR_HSV2BGR)
|
||||
image._imageData = cv2_to_pil(bgr)
|
||||
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
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
|
||||
|
||||
@@ -1,4 +1,31 @@
|
||||
from ImageManipulation.CropImage import *
|
||||
|
||||
def GetImageManipulationList() -> list:
|
||||
return [CropImage()]
|
||||
from .CropImage import CropImage
|
||||
from .ResizeImage import ResizeImage
|
||||
from .RotateImage import RotateImage
|
||||
from .FlipImage import FlipImage
|
||||
from .ColorAdjust import ColorAdjust
|
||||
from .Filters import GaussianBlur, SobelEdge, BinaryThreshold, HistogramThreshold
|
||||
from .Padding import Padding
|
||||
from .Grayscale import Grayscale
|
||||
from .HSV import HSV
|
||||
from .HueShift import HueShift
|
||||
from .BoxBlur import BoxBlur
|
||||
from .CopyImage import CopyImage
|
||||
|
||||
def GetImageManipulationList() -> list:
|
||||
return [
|
||||
CropImage(),
|
||||
ResizeImage(),
|
||||
RotateImage(),
|
||||
FlipImage(),
|
||||
ColorAdjust(),
|
||||
Padding(),
|
||||
Grayscale(),
|
||||
HSV(),
|
||||
HueShift(),
|
||||
BoxBlur(),
|
||||
CopyImage(),
|
||||
GaussianBlur(),
|
||||
SobelEdge(),
|
||||
BinaryThreshold(),
|
||||
HistogramThreshold(),
|
||||
]
|
||||
33
src/ImageManipulation/Padding.py
Normal file
33
src/ImageManipulation/Padding.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
|
||||
|
||||
class Padding(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Padding"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["border_width"]
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
border_width = 50
|
||||
if isinstance(parameters, dict) and parameters.get("border_width") is not None:
|
||||
border_width = int(parameters.get("border_width"))
|
||||
padded = cv2.copyMakeBorder(
|
||||
cv_img,
|
||||
top=border_width,
|
||||
bottom=border_width,
|
||||
left=border_width,
|
||||
right=border_width,
|
||||
borderType=cv2.BORDER_REFLECT,
|
||||
)
|
||||
image._imageData = cv2_to_pil(padded)
|
||||
|
||||
|
||||
31
src/ImageManipulation/ResizeImage.py
Normal file
31
src/ImageManipulation/ResizeImage.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
|
||||
|
||||
class ResizeImage(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Resize"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["width", "height"]
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
width = None
|
||||
height = None
|
||||
if isinstance(parameters, dict):
|
||||
width = parameters.get("width")
|
||||
height = parameters.get("height")
|
||||
if not width or not height:
|
||||
# Default: scale to half
|
||||
w, h = pil_img.size
|
||||
width, height = max(1, w // 2), max(1, h // 2)
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
resized = cv2.resize(cv_img, (int(width), int(height)), interpolation=cv2.INTER_AREA)
|
||||
image._imageData = cv2_to_pil(resized)
|
||||
|
||||
28
src/ImageManipulation/RotateImage.py
Normal file
28
src/ImageManipulation/RotateImage.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from .ImageManipulation import ImageManipulation
|
||||
from ImageContainer import ImageContainer
|
||||
from typing import Any, List
|
||||
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||
import cv2
|
||||
|
||||
|
||||
class RotateImage(ImageManipulation):
|
||||
def getManipulationName(self) -> str:
|
||||
return "Rotate"
|
||||
|
||||
def getParameters(self) -> List[str]:
|
||||
return ["angle"]
|
||||
|
||||
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||
if image is None or image.getImage() is None:
|
||||
return
|
||||
pil_img = image.getImage()
|
||||
angle = 90
|
||||
if isinstance(parameters, dict) and parameters.get("angle") is not None:
|
||||
angle = int(parameters.get("angle"))
|
||||
cv_img = pil_to_cv2(pil_img)
|
||||
(h, w) = cv_img.shape[:2]
|
||||
center = (w // 2, h // 2)
|
||||
M = cv2.getRotationMatrix2D(center, angle, 1.0)
|
||||
rotated = cv2.warpAffine(cv_img, M, (w, h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
|
||||
image._imageData = cv2_to_pil(rotated)
|
||||
|
||||
19
src/Main.py
19
src/Main.py
@@ -1,6 +1,13 @@
|
||||
#!/usr/bin/python
|
||||
from GUI import GUI
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui = GUI()
|
||||
gui.initialise()
|
||||
#!/usr/bin/python
|
||||
import os
|
||||
import sys
|
||||
|
||||
CURRENT_DIR = os.path.dirname(__file__)
|
||||
if CURRENT_DIR not in sys.path:
|
||||
sys.path.insert(0, CURRENT_DIR)
|
||||
|
||||
from GUI import GUI
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui = GUI()
|
||||
gui.initialise()
|
||||
|
||||
44
src/utils/image_utils.py
Normal file
44
src/utils/image_utils.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def pil_to_cv2(pil_image: Image.Image) -> np.ndarray:
|
||||
"""Convert PIL Image to OpenCV image (always BGR 3-channel when color)."""
|
||||
mode = pil_image.mode
|
||||
arr = np.array(pil_image)
|
||||
|
||||
# Handle grayscale
|
||||
if mode in ("1", "L"):
|
||||
# arr is 2D. Convert to 3-channel BGR for downstream ops expecting color
|
||||
return cv2.cvtColor(arr, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
# Handle images with alpha channel by dropping alpha for processing
|
||||
if mode in ("LA", "RGBA"):
|
||||
# Convert to RGB first
|
||||
pil_rgb = pil_image.convert("RGB")
|
||||
arr = np.array(pil_rgb)
|
||||
return cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
|
||||
|
||||
# Assume RGB-like
|
||||
if arr.ndim == 3 and arr.shape[2] == 3:
|
||||
return cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
|
||||
|
||||
# Fallback: if still single-channel, expand to BGR
|
||||
if arr.ndim == 2:
|
||||
return cv2.cvtColor(arr, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
return arr
|
||||
|
||||
|
||||
def cv2_to_pil(cv_image: np.ndarray) -> Image.Image:
|
||||
"""Convert OpenCV image (BGR or GRAY) to PIL Image (RGB or L)."""
|
||||
if cv_image.ndim == 2:
|
||||
return Image.fromarray(cv_image)
|
||||
rgb = cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB)
|
||||
return Image.fromarray(rgb)
|
||||
|
||||
|
||||
def clamp_int(value: float, min_value: int = 0, max_value: int = 255) -> int:
|
||||
return int(max(min_value, min(max_value, round(value))))
|
||||
|
||||
Reference in New Issue
Block a user