From 2e5bda453194618b4a0fd00a8797e674a8501033 Mon Sep 17 00:00:00 2001 From: vb Date: Sat, 25 Oct 2025 22:17:47 +0200 Subject: [PATCH] adding area selection --- src/GUI.py | 578 ++++++++++++++++------ src/ImageManipulation/ManipulationList.py | 60 +-- src/SelectionArea.py | 72 +++ 3 files changed, 529 insertions(+), 181 deletions(-) create mode 100644 src/SelectionArea.py diff --git a/src/GUI.py b/src/GUI.py index 0be7de7..c20ec3a 100644 --- a/src/GUI.py +++ b/src/GUI.py @@ -1,151 +1,427 @@ -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="Test", 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())) - - - 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()) - - # 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('', 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() \ No newline at end of file +import tkinter as tk +from tkinter import filedialog, messagebox +from PIL import ImageTk +from ImageContainer import ImageContainer +from ImageManipulation.ManipulationList import * +from SelectionArea import SelectionArea +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 + + # Interactive selection variables + _selectionMode = False + _selectionStartX = None + _selectionStartY = None + _selectionEndX = None + _selectionEndY = None + _selectionRectangle = None + _selectionArea = 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) + + 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(), + 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())) + + + 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()) + edit_menu.add_separator() + edit_menu.add_command(label="Toggle Selection Mode", accelerator="Ctrl+S", command=lambda: self._toggleSelectionMode()) + + # Frame to hold image + imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2) + imgframe.pack(side="top", pady=10) + + # Canvas to display image and draw selection + self._imageCanvas = tk.Canvas(imgframe, width=500, height=500, bg="white") + self._imageCanvas.pack(expand=True) + + # Label to hold the image (will be placed on canvas) + self._imageLabel = tk.Label(self._imageCanvas, bg="white") + + # Bind mouse events for interactive selection + self._imageCanvas.bind("", self._onMouseClick) + self._imageCanvas.bind("", self._onMouseDrag) + self._imageCanvas.bind("", self._onMouseRelease) + self._imageCanvas.bind("", self._onRightClick) # Right-click for context menu + + self._root = root + + # Initialize selection area + self._selectionArea = SelectionArea() + + # Key bindings + root.bind_all('', lambda event: self._undo()) + root.bind_all('', lambda event: self._toggleSelectionMode()) + + 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 + + # Clear canvas + 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 _toggleSelectionMode(self) -> None: + """Toggle interactive selection mode on/off.""" + self._selectionMode = not self._selectionMode + if self._selectionMode: + self._root.config(cursor="crosshair") + messagebox.showinfo("Selection Mode", "Selection mode enabled. Click and drag to select area, then right-click for manipulation options.") + else: + self._root.config(cursor="") + self._clearSelection() + messagebox.showinfo("Selection Mode", "Selection mode disabled.") + + 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 rectangle + if self._selectionRectangle: + self._imageCanvas.delete(self._selectionRectangle) + + # Draw new rectangle with colored edges only + 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"] + ) + messagebox.showinfo("Selection", "Area selected! Right-click for manipulation options.") + + 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 + self._imageCanvas.create_rectangle( + canvas_left, canvas_top, canvas_right, canvas_bottom, + outline="red", width=3, fill="" + ) \ No newline at end of file diff --git a/src/ImageManipulation/ManipulationList.py b/src/ImageManipulation/ManipulationList.py index 3cee483..4a670d9 100644 --- a/src/ImageManipulation/ManipulationList.py +++ b/src/ImageManipulation/ManipulationList.py @@ -1,31 +1,31 @@ -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(), +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(), ] \ No newline at end of file diff --git a/src/SelectionArea.py b/src/SelectionArea.py new file mode 100644 index 0000000..5d07a3e --- /dev/null +++ b/src/SelectionArea.py @@ -0,0 +1,72 @@ +from PIL import Image +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 + + 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 + + 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(): + 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() + result_image.paste(processed_selection, (self.left, self.top)) + return result_image + return image