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

View File

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

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 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"

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

View File

@@ -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(),
]

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