712 lines
31 KiB
Python
712 lines
31 KiB
Python
import tkinter as tk
|
|
from tkinter import filedialog, colorchooser, messagebox
|
|
from PIL import ImageTk, ImageDraw, Image
|
|
from ImageContainer import ImageContainer
|
|
from ImageManipulation.ManipulationList import *
|
|
from ImageManipulation.Filters import GaussianBlur, SobelEdge, BinaryThreshold, HistogramThreshold
|
|
from ImageManipulation.ZoomImage import ZoomImage
|
|
from ImageManipulation.FlipImage import FlipImage
|
|
from ImageManipulation.RotateImage import RotateImage
|
|
from SelectionArea import SelectionArea
|
|
from utils.area_selection import AreaSelectionHandler
|
|
from utils.brush import BrushHandler
|
|
from utils.text_entry import TextEntryHandler
|
|
from utils.file_operations import FileOperationsHandler
|
|
from utils.image_properties import ImagePropertiesHandler
|
|
from utils.filter_params import FilterParamsHandler
|
|
from ImageManipulation.CopyToClipboard import CopyToClipboard
|
|
from ImageManipulation.PasteFromClipboard import PasteFromClipboard
|
|
from ImageManipulation.CutToClipboard import CutToClipboard
|
|
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
|
|
|
|
# Area selection handler (will be initialized in initialise())
|
|
_areaSelectionHandler = None
|
|
|
|
# Brush handler (will be initialized in initialise())
|
|
_brushHandler = None
|
|
|
|
# Text entry handler (will be initialized in initialise())
|
|
_textEntryHandler = None
|
|
|
|
# File operations handler (will be initialized in initialise())
|
|
_fileOperationsHandler = None
|
|
|
|
# Image properties handler (will be initialized in initialise())
|
|
_imagePropertiesHandler = None
|
|
|
|
# Filter parameters handler (will be initialized in initialise())
|
|
_filterParamsHandler = None
|
|
|
|
_canvasImageWidth = None # Store canvas image dimensions
|
|
_canvasImageHeight = 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("1100x900")
|
|
root.config(bg="white")
|
|
icon = tk.PhotoImage(file='icon.png')
|
|
root.tk.call('wm', 'iconphoto', root._w, icon)
|
|
root.minsize(1100, 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="New", accelerator="Ctrl+N", command=lambda: self._newImage())
|
|
file_menu.add_command(label="Open", accelerator="Ctrl+O", command=lambda: self._openImage())
|
|
file_menu.add_command(label="Open Camera", accelerator="Ctrl+K", command=lambda: self._openCamera())
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Save", accelerator="Ctrl+S", command=lambda: self._saveImage())
|
|
file_menu.add_command(label="Save As", command=lambda: self._saveImageAs())
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Properties", command=lambda: self._showProperties())
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Exit", accelerator="Ctrl+Q", 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)
|
|
)
|
|
|
|
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_command(label="Redo", accelerator="Ctrl+Y", command=lambda: self._redo())
|
|
edit_menu.add_separator()
|
|
edit_menu.add_command(label="Toggle Selection Mode", accelerator="Ctrl+Shift+S", command=lambda: self._toggleSelectionMode())
|
|
edit_menu.add_command(label="Toggle Selection Shape (Rect/Circle/Lasso)", accelerator="Ctrl+Shift+C", command=lambda: self._toggleSelectionShape())
|
|
edit_menu.add_separator()
|
|
edit_menu.add_command(label="Brush Tool", accelerator="Ctrl+B", command=lambda: self._toggleBrushMode())
|
|
|
|
image_menu = tk.Menu(menu, tearoff=0)
|
|
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="Circular Selection", command=lambda: self._setCircularSelection())
|
|
select_menu.add_command(label="Free-form Selection (Lasso)", command=lambda: self._setLassoSelection())
|
|
select_menu.add_command(label="Polygon Selection", command=lambda: self._setPolygonSelection())
|
|
|
|
image_menu.add_separator()
|
|
image_menu.add_command(label="Crop", command=lambda: self._cropImage())
|
|
image_menu.add_command(label="Resize", command=lambda: self._resizeImage())
|
|
image_menu.add_separator()
|
|
image_menu.add_command(label="Flip Horizontal", command=lambda: self._flipHorizontal())
|
|
image_menu.add_command(label="Flip Vertical", command=lambda: self._flipVertical())
|
|
image_menu.add_separator()
|
|
image_menu.add_command(label="Rotate Right 90°", command=lambda: self._rotateRight())
|
|
image_menu.add_command(label="Rotate Left 90°", command=lambda: self._rotateLeft())
|
|
filter_menu = tk.Menu(menu, tearoff=0)
|
|
menu.add_cascade(label="Filter", menu=filter_menu)
|
|
filter_menu.add_command(label="Gaussian Filter", command=lambda: self._applyGaussianFilter())
|
|
filter_menu.add_command(label="Sobel Filter", command=lambda: self._applySobelFilter())
|
|
filter_menu.add_command(label="Binary Filter", command=lambda: self._applyBinaryFilter())
|
|
filter_menu.add_command(label="Histogram Thresholding", command=lambda: self._applyHistogramThreshold())
|
|
clipboard_menu = tk.Menu(menu, tearoff=0)
|
|
menu.add_cascade(label="Clipboard", menu=clipboard_menu)
|
|
clipboard_menu.add_command(label="Copy Image", accelerator="Ctrl+C", command=lambda: self._copyImage())
|
|
clipboard_menu.add_command(label="Cut Image", accelerator="Ctrl+X", command=lambda: self._cutImage())
|
|
clipboard_menu.add_command(label="Paste Image", accelerator="Ctrl+V", command=lambda: self._pasteImage())
|
|
tools_menu = tk.Menu(menu, tearoff=0)
|
|
menu.add_cascade(label="Tools", menu=tools_menu)
|
|
tools_menu.add_command(label="Zoom In", accelerator="Ctrl+Plus", command=lambda: self._zoomIn())
|
|
tools_menu.add_command(label="Zoom Out", accelerator="Ctrl+Minus", command=lambda: self._zoomOut())
|
|
shapes_menu = tk.Menu(menu, tearoff=0)
|
|
menu.add_cascade(label="Shapes", menu=shapes_menu)
|
|
colors_menu = tk.Menu(menu, tearoff=0)
|
|
menu.add_cascade(label="Colors", menu=colors_menu)
|
|
|
|
|
|
|
|
# Main container frame for image and settings
|
|
main_container = tk.Frame(root, bg="white")
|
|
main_container.pack(side="top", fill="both", expand=True, padx=10, pady=10)
|
|
|
|
# Frame to hold image
|
|
imgframe = tk.Frame(main_container, width=500, height=500, bg="lightgray", relief="sunken", bd=2)
|
|
imgframe.pack(side="left", fill="both", expand=True, padx=(0, 10))
|
|
|
|
# Canvas to display image and draw selection
|
|
self._imageCanvas = tk.Canvas(imgframe, width=500, height=500, bg="white")
|
|
self._imageCanvas.pack(expand=True, fill="both")
|
|
|
|
# Label to hold the image (will be placed on canvas)
|
|
self._imageLabel = tk.Label(self._imageCanvas, bg="white")
|
|
|
|
# Bind mouse events for interactive selection and brush
|
|
self._imageCanvas.bind("<Button-1>", self._onMouseClick)
|
|
self._imageCanvas.bind("<B1-Motion>", self._onMouseDrag)
|
|
self._imageCanvas.bind("<ButtonRelease-1>", self._onMouseRelease)
|
|
self._imageCanvas.bind("<Double-Button-1>", self._onDoubleClick) # Double-click for polygon completion
|
|
self._imageCanvas.bind("<Button-3>", self._onRightClick) # Right-click for context menu
|
|
|
|
# Settings panel for brush controls
|
|
settings_panel = tk.Frame(main_container, width=250, bg="lightgray", relief="sunken", bd=2)
|
|
settings_panel.pack(side="right", fill="y", padx=(10, 0))
|
|
settings_panel.pack_propagate(False) # Prevent frame from shrinking
|
|
|
|
self._root = root
|
|
|
|
# Initialize selection area
|
|
selection_area = SelectionArea()
|
|
|
|
# Initialize area selection handler
|
|
def get_current_image():
|
|
return self._currentImage
|
|
|
|
def get_canvas_dimensions():
|
|
if self._canvasImageWidth and self._canvasImageHeight:
|
|
return (self._canvasImageWidth, self._canvasImageHeight)
|
|
else:
|
|
canvas_width = self._imageCanvas.winfo_width()
|
|
canvas_height = self._imageCanvas.winfo_height()
|
|
return (canvas_width, canvas_height)
|
|
|
|
# Area selection handler from utils/area_selection.py
|
|
self._areaSelectionHandler = AreaSelectionHandler(
|
|
canvas=self._imageCanvas,
|
|
get_current_image=get_current_image,
|
|
get_canvas_dimensions=get_canvas_dimensions,
|
|
render_image=self._renderCurrentImage,
|
|
root_window=root,
|
|
selection_area=selection_area
|
|
)
|
|
|
|
# Brush handler from utils/brush.py
|
|
self._brushHandler = BrushHandler(
|
|
canvas=self._imageCanvas,
|
|
get_current_image=get_current_image,
|
|
get_canvas_dimensions=get_canvas_dimensions,
|
|
render_image=self._renderCurrentImage,
|
|
root_window=root,
|
|
area_selection_handler=self._areaSelectionHandler
|
|
)
|
|
|
|
# Create brush UI panel
|
|
self._brushHandler.create_ui_panel(settings_panel)
|
|
|
|
# Text entry handler from utils/text_entry.py
|
|
self._textEntryHandler = TextEntryHandler(root)
|
|
|
|
# File operations handler from utils/file_operations.py
|
|
def set_current_image(image):
|
|
self._currentImage = image
|
|
|
|
self._fileOperationsHandler = FileOperationsHandler(
|
|
get_current_image=get_current_image,
|
|
set_current_image=set_current_image,
|
|
render_image=self._renderCurrentImage
|
|
)
|
|
|
|
# Image properties handler from utils/image_properties.py
|
|
self._imagePropertiesHandler = ImagePropertiesHandler(
|
|
get_current_image=get_current_image
|
|
)
|
|
|
|
# Filter parameters handler from utils/filter_params.py
|
|
self._filterParamsHandler = FilterParamsHandler(root)
|
|
|
|
# Key bindings
|
|
root.bind_all('<Control-z>', lambda event: self._undo())
|
|
root.bind_all('<Control-y>', lambda event: self._redo())
|
|
root.bind_all('<Control-n>', lambda event: self._newImage())
|
|
root.bind_all('<Control-o>', lambda event: self._openImage())
|
|
root.bind_all('<Control-s>', lambda event: self._saveImage())
|
|
root.bind_all('<Control-k>', lambda event: self._openCamera())
|
|
root.bind_all('<Control-q>', lambda event: root.quit())
|
|
root.bind_all('<Control-Shift-S>', lambda event: self._toggleSelectionMode())
|
|
root.bind_all('<Control-Shift-C>', lambda event: self._toggleSelectionShape())
|
|
root.bind_all('<Control-b>', lambda event: self._toggleBrushMode() if self._brushHandler else None)
|
|
root.bind_all('<Control-plus>', lambda event: self._zoomIn())
|
|
root.bind_all('<Control-equal>', lambda event: self._zoomIn()) # Plus key without shift
|
|
root.bind_all('<Control-minus>', lambda event: self._zoomOut())
|
|
root.bind_all('<Control-c>', lambda event: self._copyImage())
|
|
root.bind_all('<Control-x>', lambda event: self._cutImage())
|
|
root.bind_all('<Control-v>', lambda event: self._pasteImage())
|
|
|
|
root.mainloop()
|
|
|
|
def _newImage(self) -> None:
|
|
"""Create a new blank image with user-specified dimensions."""
|
|
if self._fileOperationsHandler:
|
|
self._fileOperationsHandler.new_image()
|
|
|
|
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:
|
|
# Clear canvas
|
|
self._imageCanvas.delete("all")
|
|
|
|
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
|
|
|
|
# Store canvas image dimensions for coordinate conversion
|
|
self._canvasImageWidth = width
|
|
self._canvasImageHeight = height
|
|
|
|
# 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._areaSelectionHandler and self._areaSelectionHandler.selection_area.is_active and self._areaSelectionHandler.selection_area.is_valid():
|
|
self._areaSelectionHandler.draw_selection_highlight()
|
|
|
|
def _applyManipulation(self, manipulation, params: dict | None = None) -> None:
|
|
if self._currentImage is None:
|
|
return
|
|
|
|
# Special handling for AddText - show dialog to get text input
|
|
if manipulation.getManipulationName() == "Add Text" and params is None:
|
|
if self._textEntryHandler:
|
|
params = self._textEntryHandler.show_text_input_dialog()
|
|
if params is None: # User cancelled
|
|
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:
|
|
"""Save the current image without asking for a name (uses current path)."""
|
|
if self._fileOperationsHandler:
|
|
self._fileOperationsHandler.save_image()
|
|
|
|
def _saveImageAs(self) -> None:
|
|
"""Save the current image with a new name (prompts for filename)."""
|
|
if self._fileOperationsHandler:
|
|
self._fileOperationsHandler.save_image_as()
|
|
|
|
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."""
|
|
if self._areaSelectionHandler:
|
|
self._areaSelectionHandler.toggle_selection_mode()
|
|
|
|
def _toggleSelectionShape(self) -> None:
|
|
"""Toggle selection shape between rectangle, circle, and lasso."""
|
|
if self._areaSelectionHandler:
|
|
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 _setPolygonSelection(self) -> None:
|
|
"""Set selection to polygon mode."""
|
|
if self._areaSelectionHandler:
|
|
self._areaSelectionHandler.set_polygon_selection()
|
|
|
|
def _onDoubleClick(self, event) -> None:
|
|
"""Handle double-click for polygon completion."""
|
|
if self._areaSelectionHandler:
|
|
self._areaSelectionHandler.on_double_click(event)
|
|
|
|
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 _flipHorizontal(self) -> None:
|
|
"""Flip the image horizontally."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
manipulation = FlipImage()
|
|
params = {"mode": "horizontal"}
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _flipVertical(self) -> None:
|
|
"""Flip the image vertically."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
manipulation = FlipImage()
|
|
params = {"mode": "vertical"}
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _rotateRight(self) -> None:
|
|
"""Rotate the image 90 degrees to the right (clockwise)."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
manipulation = RotateImage()
|
|
params = {"angle": -90} # Negative angle for clockwise rotation
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _rotateLeft(self) -> None:
|
|
"""Rotate the image 90 degrees to the left (counter-clockwise)."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
manipulation = RotateImage()
|
|
params = {"angle": 90} # Positive angle for counter-clockwise rotation
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _onMouseClick(self, event) -> None:
|
|
"""Handle mouse click for selection or brush."""
|
|
# Handle brush mode - delegate to handler
|
|
if self._brushHandler and self._brushHandler.on_mouse_click(event):
|
|
return
|
|
|
|
# Handle selection mode - delegate to handler
|
|
if self._areaSelectionHandler:
|
|
self._areaSelectionHandler.on_mouse_click(event)
|
|
|
|
def _onMouseDrag(self, event) -> None:
|
|
"""Handle mouse drag for selection or brush."""
|
|
# Handle brush mode - delegate to handler
|
|
if self._brushHandler and self._brushHandler.on_mouse_drag(event):
|
|
return
|
|
|
|
# Handle selection mode - delegate to handler
|
|
if self._areaSelectionHandler:
|
|
self._areaSelectionHandler.on_mouse_drag(event)
|
|
|
|
def _onMouseRelease(self, event) -> None:
|
|
"""Handle mouse release to finalize selection or stop brush."""
|
|
# Handle brush mode - delegate to handler
|
|
if self._brushHandler and self._brushHandler.on_mouse_release(event):
|
|
return
|
|
|
|
# Handle selection mode - delegate to handler
|
|
if self._areaSelectionHandler:
|
|
self._areaSelectionHandler.on_mouse_release(event)
|
|
|
|
def _onRightClick(self, event) -> None:
|
|
"""Handle right-click to show context menu for manipulation options."""
|
|
if self._areaSelectionHandler:
|
|
self._areaSelectionHandler.on_right_click(event, self._getDefaultParams)
|
|
|
|
|
|
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}
|
|
elif "text" in manipulation_name or "add text" in manipulation_name:
|
|
return {"text": "Hello World", "x": 10, "y": 10, "font_size": 40, "color": (0, 0, 0)}
|
|
else:
|
|
return {}
|
|
|
|
def _toggleBrushMode(self) -> None:
|
|
"""Toggle brush mode on/off."""
|
|
if self._brushHandler:
|
|
self._brushHandler.toggle_brush_mode()
|
|
|
|
def _showProperties(self) -> None:
|
|
"""Display image properties in a dialog."""
|
|
if self._imagePropertiesHandler:
|
|
self._imagePropertiesHandler.show_properties()
|
|
|
|
def _applyGaussianFilter(self) -> None:
|
|
"""Apply Gaussian blur filter to the image."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
# Get default parameters
|
|
default_params = self._getDefaultParams(GaussianBlur())
|
|
default_ksize = default_params.get("ksize", 5)
|
|
|
|
# Show parameter dialog
|
|
if self._filterParamsHandler:
|
|
params = self._filterParamsHandler.show_gaussian_filter_dialog(default_ksize)
|
|
if params is None: # User cancelled
|
|
return
|
|
else:
|
|
params = default_params
|
|
|
|
manipulation = GaussianBlur()
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _applySobelFilter(self) -> None:
|
|
"""Apply Sobel edge detection filter to the image."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
# Get default parameters
|
|
default_params = self._getDefaultParams(SobelEdge())
|
|
default_dx = default_params.get("dx", 1)
|
|
default_dy = default_params.get("dy", 0)
|
|
default_ksize = default_params.get("ksize", 3)
|
|
|
|
# Show parameter dialog
|
|
if self._filterParamsHandler:
|
|
params = self._filterParamsHandler.show_sobel_filter_dialog(default_dx, default_dy, default_ksize)
|
|
if params is None: # User cancelled
|
|
return
|
|
else:
|
|
params = default_params
|
|
|
|
manipulation = SobelEdge()
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _applyBinaryFilter(self) -> None:
|
|
"""Apply binary threshold filter to the image."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
# Get default parameters
|
|
default_params = self._getDefaultParams(BinaryThreshold())
|
|
default_thresh = default_params.get("thresh", 127)
|
|
|
|
# Show parameter dialog
|
|
if self._filterParamsHandler:
|
|
params = self._filterParamsHandler.show_binary_filter_dialog(default_thresh)
|
|
if params is None: # User cancelled
|
|
return
|
|
else:
|
|
params = default_params
|
|
|
|
manipulation = BinaryThreshold()
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _applyHistogramThreshold(self) -> None:
|
|
"""Apply histogram-based thresholding (Otsu's method) to the image."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
# Histogram thresholding uses Otsu's method which doesn't need parameters
|
|
manipulation = HistogramThreshold()
|
|
params = {} # No parameters needed
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _zoomIn(self) -> None:
|
|
"""Zoom in the image by a factor of 1.2."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
manipulation = ZoomImage()
|
|
params = {"scale": 1.2}
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _zoomOut(self) -> None:
|
|
"""Zoom out the image by a factor of 0.8."""
|
|
if self._currentImage is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
manipulation = ZoomImage()
|
|
params = {"scale": 0.8}
|
|
self._applyManipulation(manipulation, params)
|
|
|
|
def _copyImage(self) -> None:
|
|
"""Copy the current image to the system clipboard."""
|
|
if self._currentImage is None or self._currentImage.getImage() is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
manipulation = CopyToClipboard()
|
|
manipulation.manipulateImage(self._currentImage, {})
|
|
|
|
# Check if copy was successful
|
|
if self._currentImage._clipboard_copy_success:
|
|
messagebox.showinfo("Copy", "Image copied to clipboard.")
|
|
else:
|
|
messagebox.showerror(
|
|
"Copy Error",
|
|
"Failed to copy image to clipboard.\n\n"
|
|
"Windows: Please install pywin32 (pip install pywin32)\n"
|
|
"Linux: Please install xclip (sudo apt-get install xclip)"
|
|
)
|
|
# Clean up the attribute
|
|
delattr(self._currentImage, '_clipboard_copy_success')
|
|
|
|
def _cutImage(self) -> None:
|
|
"""Cut the current image to the system clipboard (copy and remove)."""
|
|
if self._currentImage is None or self._currentImage.getImage() is None:
|
|
messagebox.showwarning("No Image", "Please load an image first.")
|
|
return
|
|
|
|
manipulation = CutToClipboard()
|
|
manipulation.manipulateImage(self._currentImage, {})
|
|
|
|
# Check if cut was successful
|
|
if hasattr(self._currentImage, '_clipboard_cut_success'):
|
|
if self._currentImage._clipboard_cut_success:
|
|
# Re-render the canvas (will be empty now)
|
|
self._renderCurrentImage()
|
|
messagebox.showinfo("Cut", "Image cut to clipboard.")
|
|
else:
|
|
error_msg = getattr(self._currentImage, '_clipboard_cut_error',
|
|
"Failed to cut image to clipboard.")
|
|
messagebox.showerror(
|
|
"Cut Error",
|
|
f"{error_msg}\n\n"
|
|
"Windows: Please install pywin32 (pip install pywin32)\n"
|
|
"Linux: Please install xclip (sudo apt-get install xclip)"
|
|
)
|
|
# Clean up the attributes
|
|
if hasattr(self._currentImage, '_clipboard_cut_success'):
|
|
delattr(self._currentImage, '_clipboard_cut_success')
|
|
if hasattr(self._currentImage, '_clipboard_cut_error'):
|
|
delattr(self._currentImage, '_clipboard_cut_error')
|
|
|
|
def _pasteImage(self) -> None:
|
|
"""Paste an image from the system clipboard."""
|
|
manipulation = PasteFromClipboard()
|
|
|
|
# If no current image exists, create a new ImageContainer
|
|
if self._currentImage is None or self._currentImage.getImage() is None:
|
|
self._currentImage = ImageContainer()
|
|
|
|
manipulation.manipulateImage(self._currentImage, {})
|
|
|
|
# Check if paste was successful
|
|
if hasattr(self._currentImage, '_clipboard_paste_success'):
|
|
if self._currentImage._clipboard_paste_success:
|
|
# Re-render the image
|
|
self._renderCurrentImage()
|
|
messagebox.showinfo("Paste", "Image pasted from clipboard.")
|
|
else:
|
|
error_msg = getattr(self._currentImage, '_clipboard_paste_error',
|
|
"Failed to paste image from clipboard.")
|
|
messagebox.showerror(
|
|
"Paste Error",
|
|
f"{error_msg}\n\n"
|
|
"Windows: Please install pywin32 (pip install pywin32)\n"
|
|
"Linux: Please install xclip (sudo apt-get install xclip)"
|
|
)
|
|
# Clean up the attributes
|
|
if hasattr(self._currentImage, '_clipboard_paste_success'):
|
|
delattr(self._currentImage, '_clipboard_paste_success')
|
|
if hasattr(self._currentImage, '_clipboard_paste_error'):
|
|
delattr(self._currentImage, '_clipboard_paste_error')
|
|
|
|
def _openCamera(self):
|
|
camera = CameraWindow()
|
|
camera.run(self) |