From 9aee76e7fe6b584423d8043fb9166fb551aa4ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viljar=20Bergst=C3=B8l?= Date: Mon, 3 Nov 2025 16:51:55 +0100 Subject: [PATCH] adding redo and area selection --- src/GUI.py | 71 +++++++++++++++++++++++++------------------- src/SelectionArea.py | 21 +++++++++++-- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/GUI.py b/src/GUI.py index 30aec02..8567916 100644 --- a/src/GUI.py +++ b/src/GUI.py @@ -68,25 +68,12 @@ class GUI: ) # 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())) - + manual_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label="menuuuu", 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())) edit_menu = tk.Menu(menu, tearoff=0) menu.add_cascade(label="Edit", menu=edit_menu) @@ -94,6 +81,7 @@ class GUI: edit_menu.add_command(label="Redo", accelerator="Ctrl+Y", command=lambda: self._redo()) edit_menu.add_separator() edit_menu.add_command(label="Toggle Selection Mode", accelerator="Ctrl+S", command=lambda: self._toggleSelectionMode()) + edit_menu.add_command(label="Toggle Selection Shape (Rect/Circle)", accelerator="Ctrl+Shift+C", command=lambda: self._toggleSelectionShape()) # Frame to hold image imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2) @@ -102,7 +90,7 @@ class GUI: # 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") @@ -121,6 +109,7 @@ class GUI: root.bind_all('', lambda event: self._undo()) root.bind_all('', lambda event: self._redo()) root.bind_all('', lambda event: self._toggleSelectionMode()) + root.bind_all('', lambda event: self._toggleSelectionShape()) root.mainloop() @@ -201,6 +190,16 @@ class GUI: 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: @@ -218,15 +217,21 @@ class GUI: if not self._selectionMode or self._currentImage is None or self._selectionStartX is None: return - # Clear previous rectangle + # Clear previous shape 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="" - ) + # 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.""" @@ -426,7 +431,13 @@ class GUI: 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 + 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="" + ) \ No newline at end of file diff --git a/src/SelectionArea.py b/src/SelectionArea.py index 5d07a3e..07c65dc 100644 --- a/src/SelectionArea.py +++ b/src/SelectionArea.py @@ -1,4 +1,4 @@ -from PIL import Image +from PIL import Image, ImageDraw from typing import Dict, Optional, Tuple @@ -11,6 +11,7 @@ class SelectionArea: 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.""" @@ -27,6 +28,12 @@ class SelectionArea: 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.""" @@ -58,6 +65,7 @@ class SelectionArea: """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 @@ -67,6 +75,15 @@ class SelectionArea: 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)) + 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