2

Using python and the tkinter library, I want to create a program that will open an image, then draw a completely black layer on top of it, and using mouse click + movement, delete parts of that black layer (think of it as a fog-of-war in a table top game) or restore parts of the black layer.

When I want to delete part of the black layer, I basically draw a transparent shape, and when I restore the black layer, I just draw a fully black shape. Here is my current implementation:

import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk, ImageDraw

class ImageEraserApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Image Eraser App")

        # Open image file dialog
        self.image_path = filedialog.askopenfilename(
            filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp *.gif")]
        )
        if not self.image_path:
            self.root.quit()

        # Load original image and create initial resized image
        self.original_image = Image.open(self.image_path)
        self.resized_image = self.original_image.copy()
        self.tk_image = ImageTk.PhotoImage(self.resized_image)

        # Create canvas to display image
        self.canvas = tk.Canvas(self.root)
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.canvas_image = self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_image)

        # Initialize black layer for erasing and painting
        self.black_layer = Image.new('RGBA', self.original_image.size, (0, 0, 0, 255))
        self.layer_draw = ImageDraw.Draw(self.black_layer)
        self.tk_black_layer = ImageTk.PhotoImage(self.black_layer)

        # Eraser size
        self.eraser_size = 20

        # Bind mouse events
        self.canvas.bind("<B1-Motion>", self.erase)
        self.canvas.bind("<B3-Motion>", self.paint_black)
        self.canvas.bind("<MouseWheel>", self.adjust_eraser_size)

        # Display initial black layer
        self.black_layer_id = self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_black_layer)

    def adjust_eraser_size(self, event):
        # Adjust the eraser size based on mouse wheel scroll
        if event.delta > 0:
            self.eraser_size = min(100, self.eraser_size + 20)
        elif event.delta < 0:
            self.eraser_size = max(20, self.eraser_size - 20)

    def erase(self, event):
        # Draw on the black layer to "erase" it
        self.layer_draw.ellipse([
            (event.x - self.eraser_size, event.y - self.eraser_size),
            (event.x + self.eraser_size, event.y + self.eraser_size)
        ], fill=(0, 0, 0, 0))

        # Update the black layer on the canvas
        self.tk_black_layer = ImageTk.PhotoImage(self.black_layer)
        self.canvas.itemconfig(self.black_layer_id, image=self.tk_black_layer)

    def paint_black(self, event):
        # Draw on the black layer to "paint" it
        self.layer_draw.ellipse([
            (event.x - self.eraser_size, event.y - self.eraser_size),
            (event.x + self.eraser_size, event.y + self.eraser_size)
        ], fill=(0, 0, 0, 255))

        # Update the black layer on the canvas
        self.tk_black_layer = ImageTk.PhotoImage(self.black_layer)
        self.canvas.itemconfig(self.black_layer_id, image=self.tk_black_layer)


if __name__ == "__main__":
    root = tk.Tk()
    app = ImageEraserApp(root)
    root.mainloop()

Now as the number of the shapes I draw grows, the app becomes more and more unresponsive (if I try to do some resizing operations/zoom-in/zoom-out on top of this, it gets even worse (easiest to notice when loading a very large image)

My question is how can I optimize this? Is the way I am trying to implement it even the correct way to solve this kind of problem?

1
  • How much of a big image are we talking about? I tried erasing and drawing the black mask again and again. It is not getting unresponsive. You meant to say- when the mouse is moved following a certain motion the trail of the erasing circle breaks down? It is not a continuous trail? Commented Jul 8 at 22:51

1 Answer 1

0
    def update_mask(self, x, y, draw_func, color):
        draw_func([
            (x - self.eraser_size, y - self.eraser_size),
            (x + self.eraser_size, y + self.eraser_size)
        ], fill=color)

        # Update the mask image
        self.tk_mask = ImageTk.PhotoImage(self.mask)
        self.canvas.itemconfig(self.mask_image_id, image=self.tk_mask)

    def erase(self, event):
        if self.prev_x and self.prev_y:
            self.mask_draw.line([(self.prev_x, self.prev_y), (event.x, event.y)], fill=(0, 0, 0, 0), width=self.eraser_size * 2)
        self.update_mask(event.x, event.y, self.mask_draw.ellipse, (0, 0, 0, 0))
        self.prev_x = event.x
        self.prev_y = event.y
    
    def paint_black(self, event):
        if self.prev_x and self.prev_y:
            self.mask_draw.line([(self.prev_x, self.prev_y), (event.x, event.y)], fill=(0, 0, 0, 255), width=self.eraser_size * 2)
        self.update_mask(event.x, event.y, self.mask_draw.ellipse, (0, 0, 0, 255))
        self.prev_x = event.x
        self.prev_y = event.y
    
    def reset_prev_pos(self, event):
        self.prev_x = None
        self.prev_y = None

To track the past mouse location, use the prev_x and prev_y variables to draw a line connecting the previous and current coordinates.

Smooth Erasing and Painting

The erase and paint_black methods use self.mask_draw.line to draw a line from the previous to the current place. This ensures that every point between the two positions is influenced, resulting in a smooth trail. To reset the previous position after releasing the mouse button, use the reset_prev_pos method, which is connected to the <ButtonRelease-1> and <ButtonRelease-3> events.

These modifications should result in a more consistent erasing and painting experience, with continuous trails rather than discontinuous portions.

Not the answer you're looking for? Browse other questions tagged or ask your own question.