This commit is contained in:
vb
2025-11-07 13:24:30 +01:00
parent 5aba41a4f9
commit 0a83e847ca
7 changed files with 614 additions and 21 deletions

View File

@@ -3,12 +3,15 @@ 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 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 functools import partial
@@ -39,6 +42,9 @@ class GUI:
# 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
@@ -105,18 +111,25 @@ class GUI:
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())
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())
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)
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)
@@ -143,6 +156,7 @@ class GUI:
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
@@ -207,6 +221,9 @@ class GUI:
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())
@@ -219,6 +236,9 @@ class GUI:
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.mainloop()
@@ -332,6 +352,16 @@ class GUI:
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:
@@ -440,6 +470,102 @@ class GUI:
"""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 _openCamera(self):
camera = CameraWindow()