transferring functionalities from main.py to src
This commit is contained in:
79
src/GUI.py
79
src/GUI.py
@@ -3,6 +3,7 @@ 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:
|
class GUI:
|
||||||
@@ -45,22 +46,54 @@ class GUI:
|
|||||||
file_menu = tk.Menu(menu, tearoff=0)
|
file_menu = tk.Menu(menu, tearoff=0)
|
||||||
menu.add_cascade(label="File", menu=file_menu)
|
menu.add_cascade(label="File", menu=file_menu)
|
||||||
file_menu.add_command(label="Open Image", command=lambda: self._openImage())
|
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="Save Image", command=lambda: self._saveImage())
|
||||||
file_menu.add_command(label="Exit", command=root.quit)
|
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)
|
test_menu = tk.Menu(menu, tearoff=0)
|
||||||
menu.add_cascade(label="Filters", menu=test_menu)
|
menu.add_cascade(label="Filters", menu=test_menu)
|
||||||
for manipulation in GetImageManipulationList():
|
for manipulation in GetImageManipulationList():
|
||||||
test_menu.add_command(label=manipulation.getManipulationName(), command=lambda: manipulation.manipulateImage())
|
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
|
# Frame to hold image
|
||||||
imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2)
|
imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2)
|
||||||
imgframe.pack(side="top", pady=10)
|
imgframe.pack(side="top", pady=10)
|
||||||
|
|
||||||
# Label to display image
|
# Label to display image
|
||||||
image_label = tk.Label(imgframe, width=500, height=500, bg="white")
|
self._imageLabel = tk.Label(imgframe, width=500, height=500, bg="white")
|
||||||
image_label.pack(expand=True)
|
self._imageLabel.pack(expand=True)
|
||||||
|
|
||||||
|
self._root = root
|
||||||
|
|
||||||
|
# Key bindings
|
||||||
|
root.bind_all('<Control-z>', lambda event: self._undo())
|
||||||
|
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
@@ -73,3 +106,41 @@ class GUI:
|
|||||||
|
|
||||||
self._currentImage = ImageContainer()
|
self._currentImage = ImageContainer()
|
||||||
self._currentImage.loadImage(file_path)
|
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()
|
||||||
@@ -4,6 +4,7 @@ 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:
|
def loadImage(self, path: str) -> None:
|
||||||
""" Load image file from the path.
|
""" Load image file from the path.
|
||||||
@@ -14,12 +15,37 @@ class ImageContainer:
|
|||||||
height, width, channels = imgcv2.shape
|
height, width, channels = imgcv2.shape
|
||||||
|
|
||||||
# Open and resize image (optional)
|
# Open and resize image (optional)
|
||||||
_imageData = Image.open(path)
|
self._imageData = Image.open(path)
|
||||||
_imageData = _imageData.resize((height, width), Image.LANCZOS) # Resize to fit frame
|
# PIL expects (width, height)
|
||||||
|
self._imageData = self._imageData.resize((width, height), Image.LANCZOS)
|
||||||
|
|
||||||
_path = path
|
self._path = path
|
||||||
|
self._history = []
|
||||||
|
|
||||||
print("Opened image:", path)
|
print("Opened image:", path)
|
||||||
|
|
||||||
def getDimensions(self) -> tuple[int,int]:
|
def getDimensions(self) -> tuple[int,int]:
|
||||||
return self._imageData.shape
|
# 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,6 +1,7 @@
|
|||||||
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):
|
class CropImage(ImageManipulation):
|
||||||
@@ -18,7 +19,36 @@ class CropImage(ImageManipulation):
|
|||||||
image: The image to manipulate
|
image: The image to manipulate
|
||||||
parameters: Parameters for the manipulation (e.g., crop coordinates)
|
parameters: Parameters for the manipulation (e.g., crop coordinates)
|
||||||
"""
|
"""
|
||||||
pass
|
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]:
|
def getParameters(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
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,6 +1,5 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
from ImageManipulation.CropImage import *
|
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:
|
def GetImageManipulationList() -> list:
|
||||||
return [CropImage()]
|
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)
|
||||||
|
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
#!/usr/bin/python
|
#!/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
|
from GUI import GUI
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
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