Draw with mouse over webcam Python!

Hello all! I am working on an app that opens the webcam and then allows various functionality such as save image, change brightness etc.I have implemented a drawing/annotation function so the user can freely draw with the mouse. This works except my update function keeps covering the drawing with the latest image frame from the webcam.Any ideas on how to get the drawing to stay on top of each new frame from the update function?

I have tried TK lower and lift functions but they don’t seem to work. Tried labels and frames but they don’t seem to move for me!

import tkinter as tk
import random
import cv2
from PIL import Image, ImageTk, ImageEnhance
import PIL
import time
from tkinter import filedialog
import numpy as np


class App:
    def __init__(self, video_source=0):
        self.overlay_img = None
        self.painting = None
        self.photo = None
        drawing = False  # true if mouse is pressed
        pt1_x, pt1_y = None, None
        white = (255,255,255)
        x1, y1 = [0, 0]
        x2, y2 = [0, 0]
        self.appName = "Kamera"
        self.window = tk.Tk()
        self.window.title(self.appName)
        self.window.geometry("640x520+0+0")
        self.window.resizable(0, 0)
        self.window['bg'] = 'black'
        self.video_source = video_source
        self.vid = MyVideoCapture(self.video_source)
        
        self.canvas = tk.Canvas(width=self.vid.width, height=self.vid.height)
        self.canvas.pack()
        self.canvas.bind("<B1-Motion>",self.paint)

        self.btn_snapshot = tk.Button(self.window, text="Snapshot", width=5, command=self.snapshot)
        self.btn_snapshot.pack(side=tk.LEFT, anchor=tk.SW, padx=10)

        self.btn_overlay = tk.Button(self.window, text="Overlay", width=7, command=self.overlay)
        self.btn_overlay.pack(side=tk.LEFT, anchor=tk.SW, padx=10)

        self.btn_settings = tk.Button(self.window, text="Settings", width=5, command=self.settings)
        self.btn_settings.pack(side=tk.LEFT, anchor=tk.SW, padx=10)

        self.zoom_slide_value = 1
        self.slide_value = 200

        self.update()
        self.window.mainloop()

    def settings(self):
        self.newWindow = tk.Toplevel(self.window)
        self.newWindow.title("Settings")
        self.newWindow.geometry("400x400+0+0")
        #self.newWindow.attributes('-transparentcolor', 'red')
        self.newWindow.resizable(0, 0)

        #Button to mirror/flip image
        self.btn_flip = tk.Button(self.newWindow, text="Mirror Image", width=10, command=self.flip_img)
        self.btn_flip.pack(side=tk.LEFT, padx=10)

        #Zoom Slider
        self.zoom = tk.Scale(
        self.newWindow, length=200, from_=1, to=4, orient=tk.HORIZONTAL, label='Zoom', command=self.zoomslider)
        self.zoom.set(0)
        self.zoom.pack(anchor=tk.NW)

        #Brightness Slider
        self.brightness = tk.Scale(
        self.newWindow, length=200, from_=0, to=255, orient=tk.HORIZONTAL, label="Image Brightness",
                command=self.slide)
        self.brightness.set(200)
        self.brightness.pack(anchor=tk.NW)

    def paint(self, event):
        white = (255, 255, 255)
        x1, y1 = (event.x - 2), (event.y - 2)
        x2, y2 = (event.x + 2), (event.y + 2)
        self.canvas.create_oval(x1, y1, x2, y2, fill="white", outline="white")

    def zoomslider(self, var):
        self.zoom_slide_value = self.zoom.get()
        print(self.zoom_slide_value)

    def slide(self, var):
        self.slide_value = self.brightness.get()
        print(self.slide_value)

    def flip_img(self):
        self.vid.flipped = not self.vid.flipped

    def overlay(self):
        file = filedialog.askopenfile(
            mode='rb', defaultextension='.png',title="Choose Overlay Image", filetypes=[("PNG Files", '*.png')])
        if file:
            self.overlay_img = ImageTk.PhotoImage(file=file)

    def snapshot(self):
        isTrue, frame = self.vid.getFrame()

        if isTrue:
            filename = filedialog.asksaveasfilename(
                defaultextension='.jpg', title="Choose Filename", filetypes=[("JPEG Files", '*.jpg')])
            # image = "IMG-" + time.strftime("%H-%M-%S-%d-%m") + ".jpg"
            cv2.imwrite(filename, cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
            # msg = Label(self.window, text='Image Saved' + image, bg='black', fg='green').place(x=430, y=510)

        else:
            messagebox.showerror("paint says", "unable to save image ,\n something went wrong")

    def update(self):
        isTrue, frame = self.vid.getFrame()

        #Control frame brightness
        frame = cv2.normalize(frame, frame, 0, self.slide_value, cv2.NORM_MINMAX)

        #Control frame size
        width = int(self.vid.width * self.zoom_slide_value)
        height = int(self.vid.height * self.zoom_slide_value)
        dsize = (width, height)
        frame = cv2.resize(frame, dsize, interpolation = cv2.INTER_AREA)


        #Convert frame to TK and put on canvas
        if isTrue:
            self.photo = ImageTk.PhotoImage(image=PIL.Image.fromarray(frame))
            self.image = self.canvas.create_image(0, 0, image=self.photo, anchor=tk.NW)

        #Display overlay
        if self.overlay_img:
            self.canvas.create_image(0,0, image=self.overlay_img, anchor=tk.NW)

        self.window.after(10, self.update)

class MyVideoCapture:
    def __init__(self, video_source=0):
        self.vid = cv2.VideoCapture(video_source)
        if not self.vid.isOpened():
            raise ValueError("Unable to open this camera \n select another video source", video_source)

        self.width = self.vid.get(cv2.CAP_PROP_FRAME_WIDTH)
        self.height = self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)

        self.flipped = True

    def getFrame(self):
        if self.vid.isOpened():
            isTrue, frame = self.vid.read()
            if isTrue and self.flipped:
                frame = cv2.flip(frame, 1)
            if isTrue:
                return (isTrue, cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
            else:
                return (isTrue, None)
        else:
            return (isTrue, None)

    def __del__(self):
        if self.vid.isOpened():
            self.vid.release()


if __name__ == "__main__":
    App()

The “usual” way this is handled is to do your annotation drawing to a separate image surface of some sort.

Then, when a repaint happens due to a new camera frame, the draw function must:

  1. Draw the camera image
  2. Draw the annotation overlay on top of the camera image

You sort of have that idea implemented in that you allow a an overlay image loaded from a PNG file, you just need to do the same for the annotation activity.

What I don’t know off-hand is how to create an off-screen “drawing surface”. I’d guess you can create a canvas instance that isn’t associated with a window … It’s been a little while since I poked at TK

I got the answer on Stack Overflow: python - How to draw with mouse over webcam in Tkinter? - Stack Overflow

Neat! I learn something new every day! Thank you!