adding requirements for image menu; integrating area selection with crop and resize functionalities
This commit is contained in:
23
README.md
23
README.md
@@ -22,20 +22,18 @@ Select
|
|||||||
- [x] Free-form selection (Lasso)
|
- [x] Free-form selection (Lasso)
|
||||||
- [ ] Polygon selection
|
- [ ] Polygon selection
|
||||||
- [x] Crop
|
- [x] Crop
|
||||||
- [ ] Resize
|
- [x] Resize
|
||||||
|
- [ ] Rotate right 90 degrees
|
||||||
Rotate
|
- [ ] Rotate Left 90 degrees
|
||||||
- [x] Rotate right 90 degrees
|
- [ ] Flip vertical
|
||||||
- [x] Rotate Left 90 degrees
|
- [ ] Flip horizontal
|
||||||
- [x] Flip vertical
|
|
||||||
- [x] Flip horizontal
|
|
||||||
|
|
||||||
Tools menu
|
Tools menu
|
||||||
- [ ] Zoom (Zoom In, Zoom Out)
|
- [ ] Zoom (Zoom In, Zoom Out)
|
||||||
- [ ] Erase
|
- [ ] Erase
|
||||||
- [x] Color Picker
|
- [ ] Color Picker
|
||||||
- [ ] Paint brushes (with different textures/patterns)
|
- [ ] Paint brushes (with different textures/patterns)
|
||||||
- [x] Text box
|
- [ ] Text box
|
||||||
|
|
||||||
Filters
|
Filters
|
||||||
- [ ] Gaussian filter
|
- [ ] Gaussian filter
|
||||||
@@ -50,10 +48,11 @@ Shapes menu
|
|||||||
|
|
||||||
Colors menu
|
Colors menu
|
||||||
- [ ] Color pallet
|
- [ ] Color pallet
|
||||||
- [x] Size of brush
|
- [ ] Size of brush
|
||||||
|
|
||||||
### Optional feature to design:
|
### Optional features
|
||||||
- [x] Snapchat filters
|
- [x] Camera
|
||||||
|
- [x] Snapchat camera filters
|
||||||
- [ ] Smart Scissors
|
- [ ] Smart Scissors
|
||||||
- [ ] Geofilters
|
- [ ] Geofilters
|
||||||
|
|
||||||
|
|||||||
62
src/GUI.py
62
src/GUI.py
@@ -1,5 +1,5 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import filedialog, colorchooser
|
from tkinter import filedialog, colorchooser, messagebox
|
||||||
from PIL import ImageTk, ImageDraw, Image
|
from PIL import ImageTk, ImageDraw, Image
|
||||||
from ImageContainer import ImageContainer
|
from ImageContainer import ImageContainer
|
||||||
from ImageManipulation.ManipulationList import *
|
from ImageManipulation.ManipulationList import *
|
||||||
@@ -100,6 +100,17 @@ class GUI:
|
|||||||
|
|
||||||
image_menu = tk.Menu(menu, tearoff=0)
|
image_menu = tk.Menu(menu, tearoff=0)
|
||||||
menu.add_cascade(label="Image", menu=image_menu)
|
menu.add_cascade(label="Image", menu=image_menu)
|
||||||
|
|
||||||
|
# Select submenu
|
||||||
|
select_menu = tk.Menu(image_menu, tearoff=0)
|
||||||
|
image_menu.add_cascade(label="Select", menu=select_menu)
|
||||||
|
select_menu.add_command(label="Rectangular Selection", command=lambda: self._setRectangularSelection())
|
||||||
|
select_menu.add_command(label="Free-form Selection (Lasso)", command=lambda: self._setLassoSelection())
|
||||||
|
select_menu.add_command(label="Circular Selection", command=lambda: self._setCircularSelection())
|
||||||
|
|
||||||
|
image_menu.add_separator()
|
||||||
|
image_menu.add_command(label="Crop", command=lambda: self._cropImage())
|
||||||
|
image_menu.add_command(label="Resize", command=lambda: self._resizeImage())
|
||||||
filter_menu = tk.Menu(menu, tearoff=0)
|
filter_menu = tk.Menu(menu, tearoff=0)
|
||||||
menu.add_cascade(label="Filter", menu=filter_menu)
|
menu.add_cascade(label="Filter", menu=filter_menu)
|
||||||
clipboard_menu = tk.Menu(menu, tearoff=0)
|
clipboard_menu = tk.Menu(menu, tearoff=0)
|
||||||
@@ -306,6 +317,55 @@ class GUI:
|
|||||||
if self._areaSelectionHandler:
|
if self._areaSelectionHandler:
|
||||||
self._areaSelectionHandler.toggle_selection_shape()
|
self._areaSelectionHandler.toggle_selection_shape()
|
||||||
|
|
||||||
|
def _setRectangularSelection(self) -> None:
|
||||||
|
"""Set selection to rectangular mode."""
|
||||||
|
if self._areaSelectionHandler:
|
||||||
|
self._areaSelectionHandler.set_rectangular_selection()
|
||||||
|
|
||||||
|
def _setCircularSelection(self) -> None:
|
||||||
|
"""Set selection to circular mode."""
|
||||||
|
if self._areaSelectionHandler:
|
||||||
|
self._areaSelectionHandler.set_circular_selection()
|
||||||
|
|
||||||
|
def _setLassoSelection(self) -> None:
|
||||||
|
"""Set selection to lasso (free-form) mode."""
|
||||||
|
if self._areaSelectionHandler:
|
||||||
|
self._areaSelectionHandler.set_lasso_selection()
|
||||||
|
|
||||||
|
def _cropImage(self) -> None:
|
||||||
|
"""Crop the image to the selected rectangular area. Starts selection mode if no selection exists."""
|
||||||
|
if not self._areaSelectionHandler:
|
||||||
|
return
|
||||||
|
|
||||||
|
# If there's already a valid selection, crop immediately
|
||||||
|
if (self._areaSelectionHandler.selection_area and
|
||||||
|
self._areaSelectionHandler.selection_area.is_active and
|
||||||
|
self._areaSelectionHandler.selection_area.is_valid()):
|
||||||
|
self._areaSelectionHandler.crop_to_selection()
|
||||||
|
else:
|
||||||
|
# Set up rectangular selection mode and callback
|
||||||
|
self._areaSelectionHandler.set_rectangular_selection()
|
||||||
|
self._areaSelectionHandler.set_pending_operation_callback(
|
||||||
|
lambda: self._areaSelectionHandler.crop_to_selection()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _resizeImage(self) -> None:
|
||||||
|
"""Resize the image to match the selected rectangular area dimensions. Starts selection mode if no selection exists."""
|
||||||
|
if not self._areaSelectionHandler:
|
||||||
|
return
|
||||||
|
|
||||||
|
# If there's already a valid selection, resize immediately
|
||||||
|
if (self._areaSelectionHandler.selection_area and
|
||||||
|
self._areaSelectionHandler.selection_area.is_active and
|
||||||
|
self._areaSelectionHandler.selection_area.is_valid()):
|
||||||
|
self._areaSelectionHandler.resize_to_selection_dimensions()
|
||||||
|
else:
|
||||||
|
# Set up rectangular selection mode and callback
|
||||||
|
self._areaSelectionHandler.set_rectangular_selection()
|
||||||
|
self._areaSelectionHandler.set_pending_operation_callback(
|
||||||
|
lambda: self._areaSelectionHandler.resize_to_selection_dimensions()
|
||||||
|
)
|
||||||
|
|
||||||
def _onMouseClick(self, event) -> None:
|
def _onMouseClick(self, event) -> None:
|
||||||
"""Handle mouse click for selection or brush."""
|
"""Handle mouse click for selection or brush."""
|
||||||
# Handle brush mode - delegate to handler
|
# Handle brush mode - delegate to handler
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ class AreaSelectionHandler:
|
|||||||
self._lassoPath = [] # List of canvas coordinates for lasso drawing
|
self._lassoPath = [] # List of canvas coordinates for lasso drawing
|
||||||
self._lassoLine = None # Canvas line item for lasso path
|
self._lassoLine = None # Canvas line item for lasso path
|
||||||
|
|
||||||
|
# Pending operation callback (called when selection is completed)
|
||||||
|
self._pendingOperationCallback = None
|
||||||
|
|
||||||
def toggle_selection_mode(self) -> None:
|
def toggle_selection_mode(self) -> None:
|
||||||
"""Toggle interactive selection mode on/off."""
|
"""Toggle interactive selection mode on/off."""
|
||||||
self._selectionMode = not self._selectionMode
|
self._selectionMode = not self._selectionMode
|
||||||
@@ -65,6 +68,7 @@ class AreaSelectionHandler:
|
|||||||
self._root.config(cursor="crosshair")
|
self._root.config(cursor="crosshair")
|
||||||
else:
|
else:
|
||||||
self._root.config(cursor="")
|
self._root.config(cursor="")
|
||||||
|
self._pendingOperationCallback = None # Clear pending callback when disabling selection mode
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def toggle_selection_shape(self) -> None:
|
def toggle_selection_shape(self) -> None:
|
||||||
@@ -86,6 +90,44 @@ class AreaSelectionHandler:
|
|||||||
if self._selectionArea.is_active and self._selectionArea.is_valid():
|
if self._selectionArea.is_active and self._selectionArea.is_valid():
|
||||||
self._render_image()
|
self._render_image()
|
||||||
|
|
||||||
|
def set_selection_shape(self, shape: str) -> None:
|
||||||
|
"""Set the selection shape directly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shape: One of "rectangle", "circle", or "lasso"
|
||||||
|
"""
|
||||||
|
if self._selectionArea is None:
|
||||||
|
return
|
||||||
|
if shape in ("rectangle", "circle", "lasso"):
|
||||||
|
self._selectionArea.set_shape(shape)
|
||||||
|
self._selectionType = shape
|
||||||
|
# Enable selection mode if not already enabled
|
||||||
|
if not self._selectionMode:
|
||||||
|
self.toggle_selection_mode()
|
||||||
|
# Refresh selection drawing if active
|
||||||
|
if self._selectionArea.is_active and self._selectionArea.is_valid():
|
||||||
|
self._render_image()
|
||||||
|
|
||||||
|
def set_rectangular_selection(self) -> None:
|
||||||
|
"""Set selection to rectangular mode and enable selection."""
|
||||||
|
self.set_selection_shape("rectangle")
|
||||||
|
|
||||||
|
def set_circular_selection(self) -> None:
|
||||||
|
"""Set selection to circular mode and enable selection."""
|
||||||
|
self.set_selection_shape("circle")
|
||||||
|
|
||||||
|
def set_lasso_selection(self) -> None:
|
||||||
|
"""Set selection to lasso (free-form) mode and enable selection."""
|
||||||
|
self.set_selection_shape("lasso")
|
||||||
|
|
||||||
|
def set_pending_operation_callback(self, callback: Optional[Callable]) -> None:
|
||||||
|
"""Set a callback to be executed when a selection is completed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback: Function to call when selection is completed, or None to clear
|
||||||
|
"""
|
||||||
|
self._pendingOperationCallback = callback
|
||||||
|
|
||||||
def on_mouse_click(self, event) -> bool:
|
def on_mouse_click(self, event) -> bool:
|
||||||
"""Handle mouse click for selection.
|
"""Handle mouse click for selection.
|
||||||
|
|
||||||
@@ -212,6 +254,12 @@ class AreaSelectionHandler:
|
|||||||
|
|
||||||
# Redraw the selection highlight to ensure it's visible
|
# Redraw the selection highlight to ensure it's visible
|
||||||
self._render_image()
|
self._render_image()
|
||||||
|
|
||||||
|
# Check if there's a pending operation callback and execute it
|
||||||
|
if self._pendingOperationCallback and self._selectionArea.is_valid():
|
||||||
|
callback = self._pendingOperationCallback
|
||||||
|
self._pendingOperationCallback = None # Clear callback to prevent multiple calls
|
||||||
|
callback()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Handle rectangle/circle mode
|
# Handle rectangle/circle mode
|
||||||
@@ -236,6 +284,12 @@ class AreaSelectionHandler:
|
|||||||
)
|
)
|
||||||
# Redraw the selection highlight to ensure it's visible
|
# Redraw the selection highlight to ensure it's visible
|
||||||
self._render_image()
|
self._render_image()
|
||||||
|
|
||||||
|
# Check if there's a pending operation callback and execute it
|
||||||
|
if self._pendingOperationCallback and self._selectionArea.is_valid():
|
||||||
|
callback = self._pendingOperationCallback
|
||||||
|
self._pendingOperationCallback = None # Clear callback to prevent multiple calls
|
||||||
|
callback()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def on_right_click(self, event, get_default_params: Callable) -> bool:
|
def on_right_click(self, event, get_default_params: Callable) -> bool:
|
||||||
@@ -391,6 +445,56 @@ class AreaSelectionHandler:
|
|||||||
# Silently handle errors - crop operation failed
|
# Silently handle errors - crop operation failed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def resize_to_selection_dimensions(self) -> None:
|
||||||
|
"""Resize the entire image to match the selection area dimensions."""
|
||||||
|
current_image = self._get_current_image()
|
||||||
|
if current_image is None or not self._selectionArea or not self._selectionArea.is_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the selection coordinates
|
||||||
|
coords = self._selectionArea.get_coordinates()
|
||||||
|
if not coords:
|
||||||
|
return
|
||||||
|
|
||||||
|
left, top, right, bottom = coords
|
||||||
|
|
||||||
|
# Calculate dimensions from selection
|
||||||
|
width = right - left
|
||||||
|
height = bottom - top
|
||||||
|
|
||||||
|
# Validate dimensions
|
||||||
|
if width <= 0 or height <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the original image
|
||||||
|
original_image = current_image.getImage()
|
||||||
|
if original_image is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Take undo snapshot
|
||||||
|
current_image.snapshot()
|
||||||
|
|
||||||
|
# Resize the entire image to the selection dimensions
|
||||||
|
from utils.image_utils import pil_to_cv2, cv2_to_pil
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
cv_img = pil_to_cv2(original_image)
|
||||||
|
resized = cv2.resize(cv_img, (int(width), int(height)), interpolation=cv2.INTER_AREA)
|
||||||
|
resized_image = cv2_to_pil(resized)
|
||||||
|
|
||||||
|
# Update the current image
|
||||||
|
current_image._imageData = resized_image
|
||||||
|
|
||||||
|
# Clear selection before re-rendering
|
||||||
|
self.clear_selection()
|
||||||
|
|
||||||
|
# Re-render the image (without selection)
|
||||||
|
self._render_image()
|
||||||
|
except Exception as e:
|
||||||
|
# Silently handle errors - resize operation failed
|
||||||
|
pass
|
||||||
|
|
||||||
def apply_manipulation_to_selection(self, manipulation, get_default_params: Callable) -> None:
|
def apply_manipulation_to_selection(self, manipulation, get_default_params: Callable) -> None:
|
||||||
"""Apply the specified manipulation to the selected area only."""
|
"""Apply the specified manipulation to the selected area only."""
|
||||||
current_image = self._get_current_image()
|
current_image = self._get_current_image()
|
||||||
|
|||||||
Reference in New Issue
Block a user