Merge pull request #9 from UiA-IKT213-Gruppe4/area-selection

Area selection
This commit is contained in:
viljarb0
2025-11-03 21:20:08 +01:00
committed by GitHub
6 changed files with 592 additions and 173 deletions

View File

@@ -1,132 +1,443 @@
import tkinter as tk import tkinter as tk
from tkinter import filedialog from tkinter import filedialog, messagebox
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 from SelectionArea import SelectionArea
from functools import partial
class GUI:
"""The GUI class responsible for the main application GUI.
class GUI:
@warning This class is a singleton. """The GUI class responsible for the main application GUI.
"""
_instance = None @warning This class is a singleton.
"""
# If the GUI has been initialised. _instance = None
_isInitialised = False
# If the GUI has been initialised.
_currentImage = None _isInitialised = False
def __new__(cls): _currentImage = None
if cls._instance is None:
cls._instance = super(GUI, cls).__new__(cls) # Interactive selection variables
return cls._instance _selectionMode = False
_selectionStartX = None
def initialise(self): _selectionStartY = None
"""Initialise the GUI.""" _selectionEndX = None
_selectionEndY = None
if self._isInitialised: _selectionRectangle = None
return _selectionArea = None
else:
self._isInitialised = True def __new__(cls):
if cls._instance is None:
# Main window cls._instance = super(GUI, cls).__new__(cls)
root = tk.Tk() return cls._instance
root.title("Image Viewer")
root.geometry("800x600") def initialise(self):
root.config(bg="white") """Initialise the GUI."""
icon = tk.PhotoImage(file='icon.png')
root.tk.call('wm', 'iconphoto', root._w, icon) if self._isInitialised:
root.minsize(800, 600) return
else:
# Menus self._isInitialised = True
menu = tk.Menu(root)
root.config(menu=menu) # Main window
file_menu = tk.Menu(menu, tearoff=0) root = tk.Tk()
menu.add_cascade(label="File", menu=file_menu) root.title("Image Viewer")
file_menu.add_command(label="Open Image", command=lambda: self._openImage()) root.geometry("800x600")
file_menu.add_command(label="Open Camera", command=lambda: self._openCamera()) root.config(bg="white")
file_menu.add_command(label="Save Image", command=lambda: self._saveImage()) icon = tk.PhotoImage(file='icon.png')
file_menu.add_command(label="Exit", command=root.quit) root.tk.call('wm', 'iconphoto', root._w, icon)
root.minsize(800, 600)
edit_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Edit", menu=edit_menu) # Menus
edit_menu.add_command(label="Undo", accelerator="Ctrl+Z", command=lambda: self._undo()) menu = tk.Menu(root)
root.config(menu=menu)
file_menu = tk.Menu(menu, tearoff=0)
test_menu = tk.Menu(menu, tearoff=0) menu.add_cascade(label="File", menu=file_menu)
menu.add_cascade(label="Test", menu=test_menu) file_menu.add_command(label="Open Image", command=lambda: self._openImage())
for manipulation in GetImageManipulationList(): file_menu.add_command(label="Save Image", command=lambda: self._saveImage())
test_menu.add_command( file_menu.add_command(label="Exit", command=root.quit)
label=manipulation.getManipulationName(),
command=partial(self._applyManipulation, manipulation) test_menu = tk.Menu(menu, tearoff=0)
) menu.add_cascade(label="Test", menu=test_menu)
for manipulation in GetImageManipulationList():
test_menu.add_command(
label=manipulation.getManipulationName(),
# Frame to hold image command=partial(self._applyManipulation, manipulation)
imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2) )
imgframe.pack(side="top", pady=10)
# Manual Test menu listing all manipulations explicitly
# Label to display image manual_menu = tk.Menu(menu, tearoff=0)
self._imageLabel = tk.Label(imgframe, width=500, height=500, bg="white") menu.add_cascade(label="menuuuu", menu=manual_menu)
self._imageLabel.pack(expand=True) 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()))
self._root = root 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()))
# Key bindings
root.bind_all('<Control-z>', lambda event: self._undo()) edit_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Edit", menu=edit_menu)
root.mainloop() edit_menu.add_command(label="Undo", accelerator="Ctrl+Z", command=lambda: self._undo())
edit_menu.add_command(label="Redo", accelerator="Ctrl+Y", command=lambda: self._redo())
def _openImage(self) -> None: edit_menu.add_separator()
file_path = filedialog.askopenfilename( edit_menu.add_command(label="Toggle Selection Mode", accelerator="Ctrl+S", command=lambda: self._toggleSelectionMode())
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")] edit_menu.add_command(label="Toggle Selection Shape (Rect/Circle)", accelerator="Ctrl+Shift+C", command=lambda: self._toggleSelectionShape())
)
if not file_path: # Frame to hold image
return imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2)
imgframe.pack(side="top", pady=10)
self._currentImage = ImageContainer()
self._currentImage.loadImage(file_path) # Canvas to display image and draw selection
self._renderCurrentImage() self._imageCanvas = tk.Canvas(imgframe, width=500, height=500, bg="white")
self._imageCanvas.pack(expand=True)
def _renderCurrentImage(self) -> None:
if self._currentImage is None or self._currentImage.getImage() is None: # Label to hold the image (will be placed on canvas)
return self._imageLabel = tk.Label(self._imageCanvas, bg="white")
pil_img = self._currentImage.getImage()
width, height = pil_img.size # Bind mouse events for interactive selection
tk_img = ImageTk.PhotoImage(pil_img) self._imageCanvas.bind("<Button-1>", self._onMouseClick)
# Keep reference to avoid garbage collection self._imageCanvas.bind("<B1-Motion>", self._onMouseDrag)
self._imageLabel.image = tk_img self._imageCanvas.bind("<ButtonRelease-1>", self._onMouseRelease)
self._imageLabel.config(image=tk_img, width=width, height=height) self._imageCanvas.bind("<Button-3>", self._onRightClick) # Right-click for context menu
def _applyManipulation(self, manipulation, params: dict | None = None) -> None: self._root = root
if self._currentImage is None:
return # Initialize selection area
# Take undo snapshot self._selectionArea = SelectionArea()
self._currentImage.snapshot()
if params is None: # Key bindings
# default demo params for crop root.bind_all('<Control-z>', lambda event: self._undo())
params = {"width": 200, "height": 200} root.bind_all('<Control-y>', lambda event: self._redo())
manipulation.manipulateImage(self._currentImage, params) root.bind_all('<Control-s>', lambda event: self._toggleSelectionMode())
self._renderCurrentImage() root.bind_all('<Control-Shift-C>', lambda event: self._toggleSelectionShape())
def _saveImage(self) -> None: root.mainloop()
if self._currentImage is None or self._currentImage.getImage() is None:
return def _openImage(self) -> None:
path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[ file_path = filedialog.askopenfilename(
("PNG", "*.png"), ("JPEG", "*.jpg;*.jpeg"), ("Bitmap", "*.bmp"), ("All Files", "*.*") filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")]
]) )
if not path: if not file_path:
return return
self._currentImage.saveImage(path)
self._currentImage = ImageContainer()
def _undo(self) -> None: self._currentImage.loadImage(file_path)
if self._currentImage is None: self._renderCurrentImage()
return
self._currentImage.undo() def _renderCurrentImage(self) -> None:
self._renderCurrentImage() if self._currentImage is None or self._currentImage.getImage() is None:
return
def _openCamera(self):
camera = CameraWindow() # Clear canvas
camera.run(self) self._imageCanvas.delete("all")
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
# Place image on canvas
self._imageCanvas.create_image(0, 0, anchor="nw", image=tk_img)
# Update canvas size
self._imageCanvas.config(width=width, height=height)
# If there's an active selection, draw it
if self._selectionArea.is_active and self._selectionArea.is_valid():
self._drawSelectionHighlight()
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()
def _redo(self) -> None:
if self._currentImage is None:
return
self._currentImage.redo()
self._renderCurrentImage()
def _toggleSelectionMode(self) -> None:
"""Toggle interactive selection mode on/off."""
self._selectionMode = not self._selectionMode
if self._selectionMode:
self._root.config(cursor="crosshair")
else:
self._root.config(cursor="")
self._clearSelection()
def _toggleSelectionShape(self) -> None:
"""Toggle selection shape between rectangle and circle."""
if self._selectionArea is None:
return
new_shape = "circle" if self._selectionArea.shape == "rectangle" else "rectangle"
self._selectionArea.set_shape(new_shape)
# Refresh selection drawing if active
if self._selectionArea.is_active and self._selectionArea.is_valid():
self._renderCurrentImage()
def _onMouseClick(self, event) -> None:
"""Handle mouse click for selection."""
if not self._selectionMode or self._currentImage is None:
return
# Clear any existing selection
self._clearSelection()
# Store starting coordinates
self._selectionStartX = event.x
self._selectionStartY = event.y
def _onMouseDrag(self, event) -> None:
"""Handle mouse drag for selection."""
if not self._selectionMode or self._currentImage is None or self._selectionStartX is None:
return
# Clear previous shape
if self._selectionRectangle:
self._imageCanvas.delete(self._selectionRectangle)
# Draw shape outline depending on selection shape
if self._selectionArea.shape == "circle":
self._selectionRectangle = self._imageCanvas.create_oval(
self._selectionStartX, self._selectionStartY, event.x, event.y,
outline="red", width=3, fill=""
)
else:
self._selectionRectangle = self._imageCanvas.create_rectangle(
self._selectionStartX, self._selectionStartY, event.x, event.y,
outline="red", width=3, fill=""
)
def _onMouseRelease(self, event) -> None:
"""Handle mouse release to finalize selection."""
if not self._selectionMode or self._currentImage is None or self._selectionStartX is None:
return
# Store ending coordinates
self._selectionEndX = event.x
self._selectionEndY = event.y
# Convert screen coordinates to image coordinates
selection_coords = self._convertToImageCoordinates(
self._selectionStartX, self._selectionStartY,
self._selectionEndX, self._selectionEndY
)
if selection_coords:
# Set the selection area
self._selectionArea.set_coordinates(
selection_coords["left"], selection_coords["top"],
selection_coords["right"], selection_coords["bottom"]
)
def _onRightClick(self, event) -> None:
"""Handle right-click to show context menu for manipulation options."""
if not self._selectionArea.is_active or not self._selectionArea.is_valid():
return
# Create context menu with all available manipulations
context_menu = tk.Menu(self._root, tearoff=0)
# Add manipulation options
manipulations = GetImageManipulationList()
for manipulation in manipulations:
manipulation_name = manipulation.getManipulationName()
context_menu.add_command(
label=f"Apply {manipulation_name} to Selection",
command=lambda m=manipulation: self._applyManipulationToSelection(m)
)
context_menu.add_separator()
context_menu.add_command(label="Clear Selection", command=self._clearSelection)
# Show context menu at cursor position
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
def _convertToImageCoordinates(self, start_x, start_y, end_x, end_y):
"""Convert screen coordinates to image coordinates."""
if self._currentImage is None:
return None
pil_image = self._currentImage.getImage()
if pil_image is None:
return None
# Get image dimensions
img_width, img_height = pil_image.size
# Get canvas dimensions
canvas_width = self._imageCanvas.winfo_width()
canvas_height = self._imageCanvas.winfo_height()
if canvas_width <= 0 or canvas_height <= 0:
return None
# Calculate scaling factors
scale_x = img_width / canvas_width
scale_y = img_height / canvas_height
# Convert coordinates
left = int(min(start_x, end_x) * scale_x)
top = int(min(start_y, end_y) * scale_y)
right = int(max(start_x, end_x) * scale_x)
bottom = int(max(start_y, end_y) * scale_y)
# Ensure coordinates are within image bounds
left = max(0, min(left, img_width))
top = max(0, min(top, img_height))
right = max(left, min(right, img_width))
bottom = max(top, min(bottom, img_height))
# Only return valid selection coordinates
if right > left and bottom > top:
return {"left": left, "top": top, "right": right, "bottom": bottom}
return None
def _applyManipulationToSelection(self, manipulation) -> None:
"""Apply the specified manipulation to the selected area only."""
if self._currentImage is None or not self._selectionArea.is_active:
return
# Take undo snapshot
self._currentImage.snapshot()
# Get the original image
original_image = self._currentImage.getImage()
# Crop the selection area
selected_area = self._selectionArea.crop_image(original_image)
if selected_area is None:
return
# Create a temporary ImageContainer for the selected area
temp_container = ImageContainer()
temp_container._imageData = selected_area
# Apply manipulation to the selected area
# Use default parameters if none specified
params = self._getDefaultParams(manipulation)
manipulation.manipulateImage(temp_container, params)
# Get the processed selection
processed_selection = temp_container.getImage()
# Apply the processed selection back to the original image
result_image = self._selectionArea.apply_to_image(original_image, processed_selection)
# Update the current image
self._currentImage._imageData = result_image
# Clear selection before re-rendering
self._clearSelection()
# Re-render the image (without selection)
self._renderCurrentImage()
def _getDefaultParams(self, manipulation) -> dict:
"""Get default parameters for a manipulation."""
manipulation_name = manipulation.getManipulationName().lower()
if "crop" in manipulation_name:
return {"width": 200, "height": 200}
elif "resize" in manipulation_name:
return {"width": 200, "height": 200}
elif "rotate" in manipulation_name:
return {"angle": 90}
elif "flip" in manipulation_name:
return {"mode": "horizontal"}
elif "color" in manipulation_name or "adjust" in manipulation_name:
return {"brightness": 10, "contrast": 1.2, "saturation": 1.1}
elif "blur" in manipulation_name:
return {"ksize": 5}
elif "hue" in manipulation_name:
return {"hue": 50}
elif "padding" in manipulation_name:
return {"border_width": 50}
elif "sobel" in manipulation_name:
return {"dx": 1, "dy": 0, "ksize": 3}
elif "threshold" in manipulation_name:
return {"thresh": 127}
else:
return {}
def _clearSelection(self) -> None:
"""Clear the selection rectangle and reset selection state."""
if self._selectionRectangle:
self._imageCanvas.delete(self._selectionRectangle)
self._selectionRectangle = None
self._selectionStartX = None
self._selectionStartY = None
self._selectionEndX = None
self._selectionEndY = None
self._selectionArea.clear()
def _drawSelectionHighlight(self) -> None:
"""Draw a colored highlight over the selected area."""
if not self._selectionArea.is_active or not self._selectionArea.is_valid():
return
coords = self._selectionArea.get_coordinates()
if not coords:
return
left, top, right, bottom = coords
# Convert image coordinates to canvas coordinates
pil_image = self._currentImage.getImage()
img_width, img_height = pil_image.size
canvas_width = self._imageCanvas.winfo_width()
canvas_height = self._imageCanvas.winfo_height()
if canvas_width <= 0 or canvas_height <= 0:
return
# Calculate scaling factors
scale_x = canvas_width / img_width
scale_y = canvas_height / img_height
# Convert coordinates
canvas_left = int(left * scale_x)
canvas_top = int(top * scale_y)
canvas_right = int(right * scale_x)
canvas_bottom = int(bottom * scale_y)
# Draw colored edges only
if self._selectionArea.shape == "circle":
self._imageCanvas.create_oval(
canvas_left, canvas_top, canvas_right, canvas_bottom,
outline="red", width=3, fill=""
)
else:
self._imageCanvas.create_rectangle(
canvas_left, canvas_top, canvas_right, canvas_bottom,
outline="red", width=3, fill=""
)

View File

@@ -5,6 +5,7 @@ class ImageContainer:
_imageData = None _imageData = None
_path = None _path = None
_history = None _history = None
_redo_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,13 +15,14 @@ class ImageContainer:
imgcv2 = cv2.imread(path) imgcv2 = cv2.imread(path)
height, width, channels = imgcv2.shape height, width, channels = imgcv2.shape
# Open and resize image (optional) # Open and resize image
self._imageData = Image.open(path) self._imageData = Image.open(path)
# PIL expects (width, height) # PIL expects (width, height)
self._imageData = self._imageData.resize((width, height), Image.LANCZOS) self._imageData = self._imageData.resize((width, height), Image.LANCZOS)
self._path = path self._path = path
self._history = [] self._history = []
self._redo_history = []
print("Opened image:", path) print("Opened image:", path)
@@ -39,13 +41,31 @@ class ImageContainer:
"""Push a copy of current image to history for undo.""" """Push a copy of current image to history for undo."""
if self._imageData is None: if self._imageData is None:
return return
# Ensure a deep copy (PIL copy is sufficient) # Ensure a deep copy (but PIL copy should be sufficient)
self._history.append(self._imageData.copy()) self._history.append(self._imageData.copy())
# Cap history size to avoid memory blow-up # New action invalidates redo history
if self._redo_history is not None:
self._redo_history.clear()
# Cap history size to avoid memory blow up
if len(self._history) > 20: if len(self._history) > 20:
self._history.pop(0) self._history.pop(0)
def undo(self) -> None: def undo(self) -> None:
if not self._history: if not self._history:
return return
self._imageData = self._history.pop() # Move current state to redo stack
if self._redo_history is None:
self._redo_history = []
if self._imageData is not None:
self._redo_history.append(self._imageData.copy())
# Restore last snapshot
self._imageData = self._history.pop()
def redo(self) -> None:
if not self._redo_history:
return
# Moving forward: save current to undo history
if self._imageData is not None:
self._history.append(self._imageData.copy())
# Apply redo state
self._imageData = self._redo_history.pop()

View File

@@ -36,7 +36,6 @@ class CropImage(ImageManipulation):
right = left + crop_width right = left + crop_width
bottom = top + crop_height bottom = top + crop_height
else: else:
# Default: trim margins similar to demo in root main.py
img_width, img_height = pil_image.size img_width, img_height = pil_image.size
left = 80 left = 80
top = 80 top = 80

View File

@@ -10,7 +10,7 @@ class FlipImage(ImageManipulation):
return "Flip" return "Flip"
def getParameters(self) -> List[str]: def getParameters(self) -> List[str]:
return ["mode"] # horizontal|vertical return ["mode"]
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None: def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
if image is None or image.getImage() is None: if image is None or image.getImage() is None:

View File

@@ -1,36 +1,36 @@
from .CropImage import CropImage from .CropImage import CropImage
from .ResizeImage import ResizeImage from .ResizeImage import ResizeImage
from .RotateImage import RotateImage from .RotateImage import RotateImage
from .FlipImage import FlipImage from .FlipImage import FlipImage
from .ColorAdjust import ColorAdjust from .ColorAdjust import ColorAdjust
from .Filters import GaussianBlur, SobelEdge, BinaryThreshold, HistogramThreshold from .Filters import GaussianBlur, SobelEdge, BinaryThreshold, HistogramThreshold
from .Padding import Padding from .Padding import Padding
from .Grayscale import Grayscale from .Grayscale import Grayscale
from .HSV import HSV from .HSV import HSV
from .HueShift import HueShift from .HueShift import HueShift
from .BoxBlur import BoxBlur from .BoxBlur import BoxBlur
from .CopyImage import CopyImage from .CopyImage import CopyImage
from .BackgroundBlur import BackgroundBlur from .BackgroundBlur import BackgroundBlur
from .CameraWindow import CameraWindow from .CameraWindow import CameraWindow
def GetImageManipulationList() -> list: def GetImageManipulationList() -> list:
return [ return [
CropImage(), CropImage(),
ResizeImage(), ResizeImage(),
RotateImage(), RotateImage(),
FlipImage(), FlipImage(),
ColorAdjust(), ColorAdjust(),
Padding(), Padding(),
Grayscale(), Grayscale(),
HSV(), HSV(),
HueShift(), HueShift(),
BoxBlur(), BoxBlur(),
CopyImage(), CopyImage(),
GaussianBlur(), GaussianBlur(),
SobelEdge(), SobelEdge(),
BinaryThreshold(), BinaryThreshold(),
HistogramThreshold(), HistogramThreshold(),
BackgroundBlur(), BackgroundBlur(),
CameraWindow(), CameraWindow(),
] ]

89
src/SelectionArea.py Normal file
View File

@@ -0,0 +1,89 @@
from PIL import Image, ImageDraw
from typing import Dict, Optional, Tuple
class SelectionArea:
"""Class to handle selected rectangular areas on images."""
def __init__(self):
self.left: Optional[int] = None
self.top: Optional[int] = None
self.right: Optional[int] = None
self.bottom: Optional[int] = None
self.is_active: bool = False
self.shape: str = "rectangle" # "rectangle" or "circle"
def set_coordinates(self, left: int, top: int, right: int, bottom: int) -> None:
"""Set the selection coordinates."""
self.left = left
self.top = top
self.right = right
self.bottom = bottom
self.is_active = True
def clear(self) -> None:
"""Clear the selection."""
self.left = None
self.top = None
self.right = None
self.bottom = None
self.is_active = False
self.shape = self.shape # keep last used shape
def set_shape(self, shape: str) -> None:
"""Set the selection shape: 'rectangle' or 'circle'."""
if shape in ("rectangle", "circle"):
self.shape = shape
def get_coordinates(self) -> Optional[Tuple[int, int, int, int]]:
"""Get the selection coordinates as a tuple."""
if self.is_active and all(coord is not None for coord in [self.left, self.top, self.right, self.bottom]):
return (self.left, self.top, self.right, self.bottom)
return None
def get_dict(self) -> Optional[Dict[str, int]]:
"""Get the selection coordinates as a dictionary."""
coords = self.get_coordinates()
if coords:
return {
"left": coords[0],
"top": coords[1],
"right": coords[2],
"bottom": coords[3]
}
return None
def is_valid(self) -> bool:
"""Check if the selection is valid (has area > 0)."""
coords = self.get_coordinates()
if coords:
left, top, right, bottom = coords
return right > left and bottom > top
return False
def crop_image(self, image: Image.Image) -> Optional[Image.Image]:
"""Crop the given image to the selection area."""
coords = self.get_coordinates()
if coords and self.is_valid():
# For both rectangle and circle, return the bounding box crop
return image.crop(coords)
return None
def apply_to_image(self, image: Image.Image, processed_selection: Image.Image) -> Image.Image:
"""Apply the processed selection back to the original image."""
coords = self.get_coordinates()
if coords and self.is_valid():
# Create a copy to avoid modifying the original
result_image = image.copy()
if self.shape == "circle":
# Create circular mask for blending only inside the circle
width = self.right - self.left
height = self.bottom - self.top
mask = Image.new("L", (width, height), 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0, width, height), fill=255)
result_image.paste(processed_selection, (self.left, self.top), mask)
else:
result_image.paste(processed_selection, (self.left, self.top))
return result_image
return image