transferring functionalities from main.py to src

This commit is contained in:
vb
2025-10-20 14:37:49 +02:00
parent 8f8f4b11ce
commit 2e168f408e
18 changed files with 767 additions and 187 deletions

View File

@@ -1,75 +1,146 @@
import tkinter as tk import tkinter as tk
from tkinter import filedialog from tkinter import filedialog
from PIL import ImageTk from PIL import ImageTk
from ImageContainer import ImageContainer from ImageContainer import ImageContainer
from ImageManipulation.ManipulationList import * from ImageManipulation.ManipulationList import *
from functools import partial
class GUI:
"""The GUI class responsible for the main application GUI. class GUI:
"""The GUI class responsible for the main application GUI.
@warning This class is a singleton.
""" @warning This class is a singleton.
_instance = None """
_instance = None
# If the GUI has been initialised.
_isInitialised = False # If the GUI has been initialised.
_isInitialised = False
_currentImage = None
_currentImage = None
def __new__(cls):
if cls._instance is None: def __new__(cls):
cls._instance = super(GUI, cls).__new__(cls) if cls._instance is None:
return cls._instance cls._instance = super(GUI, cls).__new__(cls)
return cls._instance
def initialise(self):
"""Initialise the GUI.""" def initialise(self):
"""Initialise the GUI."""
if self._isInitialised:
return if self._isInitialised:
else: return
self._isInitialised = True else:
self._isInitialised = True
# Main window
root = tk.Tk() # Main window
root.title("Image Viewer") root = tk.Tk()
root.geometry("800x600") root.title("Image Viewer")
root.config(bg="white") root.geometry("800x600")
icon = tk.PhotoImage(file='icon.png') root.config(bg="white")
root.tk.call('wm', 'iconphoto', root._w, icon) icon = tk.PhotoImage(file='icon.png')
root.minsize(800, 600) root.tk.call('wm', 'iconphoto', root._w, icon)
root.minsize(800, 600)
# Menus
menu = tk.Menu(root) # Menus
root.config(menu=menu) menu = tk.Menu(root)
file_menu = tk.Menu(menu, tearoff=0) root.config(menu=menu)
menu.add_cascade(label="File", menu=file_menu) file_menu = tk.Menu(menu, tearoff=0)
file_menu.add_command(label="Open Image", command=lambda: self._openImage()) menu.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Save Image", command=lambda: _save_image(OPENED_IMAGE)) file_menu.add_command(label="Open Image", command=lambda: self._openImage())
file_menu.add_command(label="Exit", command=root.quit) file_menu.add_command(label="Save Image", command=lambda: self._saveImage())
file_menu.add_command(label="Exit", command=root.quit)
test_menu = tk.Menu(menu, tearoff=0) edit_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Filters", menu=test_menu) menu.add_cascade(label="Edit", menu=edit_menu)
for manipulation in GetImageManipulationList(): edit_menu.add_command(label="Undo", accelerator="Ctrl+Z", command=lambda: self._undo())
test_menu.add_command(label=manipulation.getManipulationName(), command=lambda: manipulation.manipulateImage())
# Frame to hold image test_menu = tk.Menu(menu, tearoff=0)
imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2) menu.add_cascade(label="Filters", menu=test_menu)
imgframe.pack(side="top", pady=10) for manipulation in GetImageManipulationList():
test_menu.add_command(
# Label to display image label=manipulation.getManipulationName(),
image_label = tk.Label(imgframe, width=500, height=500, bg="white") command=partial(self._applyManipulation, manipulation)
image_label.pack(expand=True) )
root.mainloop() # Manual Test menu listing all manipulations explicitly
manual_menu = tk.Menu(menu, tearoff=0)
def _openImage(self) -> None: menu.add_cascade(label="Test", menu=manual_menu)
file_path = filedialog.askopenfilename( manual_menu.add_command(label="Padding", command=partial(self._applyManipulation, Padding(), {"border_width": 50}))
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")] 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}))
if not file_path: manual_menu.add_command(label="Copy", command=partial(self._applyManipulation, CopyImage()))
return manual_menu.add_command(label="Greyscale", command=partial(self._applyManipulation, Grayscale()))
manual_menu.add_command(label="HSV", command=partial(self._applyManipulation, HSV()))
self._currentImage = ImageContainer() manual_menu.add_command(label="Hue Shifted", command=partial(self._applyManipulation, HueShift(), {"hue": 50}))
self._currentImage.loadImage(file_path) 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()

View File

@@ -1,25 +1,51 @@
import cv2 import cv2
from PIL import Image, ImageTk from PIL import Image, ImageTk
class ImageContainer: class ImageContainer:
_imageData = None _imageData = None
_path = None _path = None
_history = None
def loadImage(self, path: str) -> None:
""" Load image file from the path. def loadImage(self, path: str) -> None:
""" Load image file from the path.
:param path: Path to the image file.
""" :param path: Path to the image file.
imgcv2 = cv2.imread(path) """
height, width, channels = imgcv2.shape imgcv2 = cv2.imread(path)
height, width, channels = imgcv2.shape
# Open and resize image (optional)
_imageData = Image.open(path) # Open and resize image (optional)
_imageData = _imageData.resize((height, width), Image.LANCZOS) # Resize to fit frame self._imageData = Image.open(path)
# PIL expects (width, height)
_path = path self._imageData = self._imageData.resize((width, height), Image.LANCZOS)
print("Opened image:", path) self._path = path
self._history = []
def getDimensions(self) -> tuple[int,int]:
return self._imageData.shape 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()

View 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)

View 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)

View 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()

View File

@@ -1,39 +1,69 @@
from ImageManipulation.ImageManipulation import ImageManipulation from .ImageManipulation import ImageManipulation
from ImageContainer import ImageContainer from ImageContainer import ImageContainer
from typing import Any, List from typing import Any, List
from PIL import Image
class CropImage(ImageManipulation):
"""Concrete implementation for cropping images.""" class CropImage(ImageManipulation):
"""Concrete implementation for cropping images."""
def __init__(self):
# You can initialize any instance variables here def __init__(self):
self.manipulation_name = "Crop" # You can initialize any instance variables here
self.manipulation_name = "Crop"
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
""" def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
Manipulate the given image using the provided parameters. """
Manipulate the given image using the provided parameters.
Args:
image: The image to manipulate Args:
parameters: Parameters for the manipulation (e.g., crop coordinates) image: The image to manipulate
""" parameters: Parameters for the manipulation (e.g., crop coordinates)
pass """
if image is None or getattr(image, "_imageData", None) is None:
def getParameters(self) -> List[str]: return
"""
Get the list of parameters required for cropping. pil_image = image._imageData
Returns: # If parameters provided as dict with width/height, do centered crop
List of parameter names 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
return ["width", "height"] target_height = int(parameters["height"]) if parameters["height"] is not None else None
if target_width is None or target_height is None:
def getManipulationName(self) -> str: return
""" img_width, img_height = pil_image.size
Get the name of this manipulation operation. crop_width = min(target_width, img_width)
crop_height = min(target_height, img_height)
Returns: left = (img_width - crop_width) // 2
The name of the manipulation 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" return "Crop Image"

View 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)

View 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)

View 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)

View 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)

View 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)

View File

@@ -1,39 +1,38 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any from typing import Any
from ImageContainer import ImageContainer
from ImageContainer import ImageContainer
class ImageManipulation(ABC):
class ImageManipulation(ABC): """Abstract base class for image manipulation operations."""
"""Abstract base class for image manipulation operations."""
@abstractmethod
@abstractmethod def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None: """
""" Manipulate the given image using the provided parameters.
Manipulate the given image using the provided parameters.
Args:
Args: image: The image to manipulate
image: The image to manipulate parameters: Parameters for the manipulation
parameters: Parameters for the manipulation """
""" pass
pass
@abstractmethod
@abstractmethod def getParameters(self) -> list[str]:
def getParameters(self) -> list[str]: """
""" Get the list of parameters required for this manipulation.
Get the list of parameters required for this manipulation.
Returns:
Returns: List of parameter names
List of parameter names """
""" pass
pass
@abstractmethod
@abstractmethod def getManipulationName(self) -> str:
def getManipulationName(self) -> str: """
""" Get the name of this manipulation operation.
Get the name of this manipulation operation.
Returns:
Returns: The name of the manipulation
The name of the manipulation """
""" pass
pass

View File

@@ -1,4 +1,31 @@
from ImageManipulation.CropImage import * from .CropImage import CropImage
from .ResizeImage import ResizeImage
def GetImageManipulationList() -> list: from .RotateImage import RotateImage
return [CropImage()] 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(),
]

View 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)

View 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)

View 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)

View File

@@ -1,6 +1,13 @@
#!/usr/bin/python #!/usr/bin/python
from GUI import GUI import os
import sys
if __name__ == "__main__":
gui = GUI() CURRENT_DIR = os.path.dirname(__file__)
gui.initialise() 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
View 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))))