使用 Tkinter 显示和编辑 opencv 图像

Display and EDIT opencv image using Tkinter

我有一个简单的 GUI 应用程序来显示来自所选相机的图像和转换后的图像。要进行转换,我需要在图像上选择 4 个点,所以我想通过单击来选择它们。但为了做到这一点,图片必须是 opencv 图像。所以我的想法是“冻结”来自相机的图像,然后对其进行编辑(例如通过函数 set_default_points() 在其上绘制 4 个圆圈)。

这是我的代码

(目前主要的函数是show_frame(), freeze_camera() 和 set_default_points())

from tkinter import *
from tkinter import ttk
import cv2
import numpy as np
from PIL import Image, ImageTk

# ----- SET -----
GREY = "#D8D8D8"
BLUE = "#81F7F3"
AQUA = "#9CC3D5"
ICE = "#C7D3D4"

canvas_width = 500
canvas_height = 600


def camera_amount():
    '''Returns int value of available camera devices connected to the host device
    from url: https://www.codegrepper.com/code-examples/python/how+to+count+how+many+cameras+you+have+with+python
    '''
    camera = 0
    while True:
        if (cv2.VideoCapture(camera).grab()) is True:
            camera = camera + 1
        else:
            cv2.destroyAllWindows()
            return camera



# ----- default start images -----
img1 = cv2.imread("d1.png")
img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)
resized1 = cv2.resize(img1, (canvas_width, canvas_height))

img2 = cv2.imread("ipm.png")
img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)
resized2 = cv2.resize(img2, (canvas_width, canvas_height))


# ----- Application ------
class App(Tk):
    def __init__(self):
        super().__init__()

        self.title("Image TOP-DOWN Tranformation")
        self.minsize(width=1200, height=700)
        self.config(padx=5, pady=5, bg=ICE)

        im1 = Image.fromarray(resized1)
        im2 = Image.fromarray(resized2)
        self.org_img = ImageTk.PhotoImage(im1)
        self.transf_img = ImageTk.PhotoImage(im2)

        self.canvas1 = Canvas(width=500, height=600, bg=ICE, highlightthickness=2)
        self.org_image_container = self.canvas1.create_image(250, 300, anchor="center", image=self.org_img)
        self.canvas1.grid(padx=10, pady=10, row=1, rowspan=8, column=0)

        self.canvas2 = Canvas(width=500, height=600, bg=ICE, highlightthickness=2)
        self.trf_image_container = self.canvas2.create_image(250, 300, anchor="center", image=self.transf_img)
        self.canvas2.grid(padx=10, pady=10, row=1, rowspan=8, column=1)

        # ----- Labels -----
        self.label1 = Label(text="Original image")
        self.label1.grid(row=0, column=0)

        self.label2 = Label(text="Transformed image")
        self.label2.grid(row=0, column=1)

        self.label3 = Label(text="Functionalities")
        self.label3.grid(row=0, column=2)

        self.move_label = Label(text="Move points")
        self.move_label.grid(row=8, column=2, columnspan=3)

        # ----- Buttons -----

        self.selected_camera = StringVar()
        self.camera_cb = ttk.Combobox(self, textvariable=self.selected_camera)
        self.camera_cb.grid(padx=5, pady=5, row=1, column=2, columnspan=3)
        amount_of_cameras = camera_amount()
        self.camera_cb['values'] = [i for i in range(amount_of_cameras)]
        self.camera_cb.current(0)

        self.change_camera = Button(text="Change Camera", command=self.choose_camera)
        self.change_camera.grid(padx=5, pady=5, row=2, column=2, columnspan=3)

        self.freeze_cam = Button(text="Freeze camera", command=self.freeze_camera)
        self.freeze_cam.grid(padx=5, pady=5, row=3, column=2, columnspan=3)

        self.default_points = Button(text="Set default points", command=self.set_default_points)
        self.default_points.grid(padx=5, pady=5, row=4, column=2, columnspan=3)

        self.transform_button = Button(text="Transform", command=self.transform)
        self.transform_button.grid(padx=5, pady=5, row=5, column=2, columnspan=3)

        self.clear_button = Button(text="CLEAR", command=self.clear)
        self.clear_button.grid(padx=5, pady=5, row=6, column=2, columnspan=3)

        self.save_button = Button(text="SAVE", command=self.save)
        self.save_button.grid(padx=5, pady=5, row=7, column=2, columnspan=3)

        self.up_butt = Button(text="↑", command=self.up)
        self.up_butt.grid(padx=5, pady=5, row=9, column=3)

        self.down_butt = Button(text="↓", command=self.down)
        self.down_butt.grid(padx=5, pady=5, row=10, column=3)

        self.left_butt = Button(text="←", command=self.left)
        self.left_butt.grid(padx=5, pady=5, row=10, column=2, columnspan=2)

        self.right_butt = Button(text="→", command=self.right)
        self.right_butt.grid(padx=5, pady=5, row=10, column=4)

        # ----- fields -----
        self.cap = cv2.VideoCapture(int(self.selected_camera.get()))
        self.imgtk = None
        self.after_id = None
        self.resized11 = None

        self.input_points = []
        self.output_points = []

    def choose_camera(self):
        self.freeze_camera()
        self.cap = cv2.VideoCapture(int(self.selected_camera.get()))
        self.show_frame()

    # po upakowaniu w klasę po prostu zmienić jedno pole, a w funkcji odpowiedzialnej za wyświetlanie kamery dac ifa
    def freeze_camera(self):
        self.canvas1.after_cancel(self.after_id)
        # now, the image on canvas is freezed,
        # it means, that the picture is last imgtk from show_frame function,
        # but it's not like cv2 image, and it cannot be editable
        # I tried example like this, but it doesn't work
        # pil_image = PIL.Image.open('Image.jpg').convert('RGB')
        # open_cv_image = numpy.array(pil_image)
        # # Convert RGB to BGR
        # open_cv_image = open_cv_image[:, :, ::-1].copy()

    def set_default_points(self):
        default_points = [[448, 609], [580,609], [580,741], [448,741]]
        for pts in default_points:
            cv2.circle(self.imgtk, pts, 5, (0, 0, 255), -2)
        pass

    def transform(self):
        pass

    def clear(self):
        pass

    def save(self):
        pass

    def up(self):
        pass

    def down(self):
        pass

    def left(self):
        pass

    def right(self):
        pass

    def draw_circle(self, event, x, y, flags, param):
        if event == cv2.EVENT_LBUTTONDBLCLK:
            cv2.circle(self.imgtk, (x, y), 5, (255, 0, 0), -2)
            self.input_points.append([x, y])
        if event == cv2.EVENT_RBUTTONDBLCLK:
            cv2.circle(self.imgtk, (x, y), 5, (0, 0, 255), -2)
            self.output_points.append([x, y])

    def show_frame(self):
        """
        https://www.tutorialspoint.com/how-to-show-webcam-in-tkinter-window
        """
        img11 = self.cap.read()[1]
        cv2image = cv2.cvtColor(img11, cv2.COLOR_BGR2RGB)
        self.resized11 = cv2.resize(cv2image, (canvas_width, canvas_height))
        im11 = Image.fromarray(self.resized11)
        self.imgtk = ImageTk.PhotoImage(im11)
        self.canvas1.imgtk = self.imgtk
        self.canvas1.itemconfig(self.org_image_container, image=self.imgtk)
        self.after_id = self.canvas1.after(10, self.show_frame)




if __name__ == "__main__":
    app = App()
    app.show_frame()
    app.mainloop()

问题是当我尝试使用函数 set_default_points 编辑那张冻结的图片时,我发生了这个错误:

Exception in Tkinter callback
Traceback (most recent call last):
  File "D:\Python_versions\lib\tkinter\__init__.py", line 1921, in __call__
    return self.func(*args)
  File "D:\DUCKIETOWN\aplikacja_do_transformacji_obrazu\APP.py", line 146, in set_default_points
    cv2.circle(self.imgtk, pts, 5, (0, 0, 255), -2)
cv2.error: OpenCV(4.5.5) :-1: error: (-5:Bad argument) in function 'circle'
> Overload resolution failed:
>  - img is not a numpy array, neither a scalar
>  - Expected Ptr<cv::UMat> for argument 'img'

编辑:看来,实现我的目标的唯一方法是使用 opencv 图像在分离的 opencv window 上选择转换点,然后将其上传到我的 tkinter 显示 window.

这里是更正后的代码:

import tkinter.messagebox
from tkinter import *
from tkinter import ttk
import cv2
import numpy as np
from PIL import Image, ImageTk
import transform


# ----- SET -----
GREY = "#D8D8D8"
BLUE = "#81F7F3"
AQUA = "#9CC3D5"
ICE = "#C7D3D4"

canvas_width = 500
canvas_height = 600


def camera_amount() -> int:
    '''Returns int value of available camera devices connected to the host device
    from url: https://www.codegrepper.com/code-examples/python/how+to+count+how+many+cameras+you+have+with+python
    '''
    camera = 0
    while True:
        if (cv2.VideoCapture(camera).grab()) is True:
            camera = camera + 1
        else:
            cv2.destroyAllWindows()
            return camera



# ----- default start images -----
img1 = cv2.imread("d1.png")
img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)
resized1 = cv2.resize(img1, (canvas_width, canvas_height))

img2 = cv2.imread("ipm.png")
img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)
resized2 = cv2.resize(img2, (canvas_width, canvas_height))


# ----- Application ------
class App(Tk):
    def __init__(self):
        super().__init__()

        self.title("Image TOP-DOWN Tranformation")
        self.minsize(width=1200, height=700)
        self.config(padx=5, pady=5, bg=ICE)

        im1 = Image.fromarray(resized1)
        im2 = Image.fromarray(resized2)
        self.org_img = ImageTk.PhotoImage(im1)
        self.transf_img = ImageTk.PhotoImage(im2)

        self.canvas1 = Canvas(width=500, height=600, bg=ICE, highlightthickness=2)
        self.org_image_container = self.canvas1.create_image(250, 300, anchor="center", image=self.org_img)
        self.canvas1.grid(padx=10, pady=10, row=1, rowspan=8, column=0)

        self.canvas2 = Canvas(width=500, height=600, bg=ICE, highlightthickness=2)
        self.trf_image_container = self.canvas2.create_image(250, 300, anchor="center", image=self.transf_img)
        self.canvas2.grid(padx=10, pady=10, row=1, rowspan=8, column=1)

        # ----- Labels -----
        self.label1 = Label(text="Original image")
        self.label1.grid(row=0, column=0)

        self.label2 = Label(text="Transformed image")
        self.label2.grid(row=0, column=1)

        self.label3 = Label(text="Functionalities")
        self.label3.grid(row=0, column=2)

        self.move_label = Label(text="Move points")
        self.move_label.grid(row=8, column=2, columnspan=3)

        # ----- Buttons -----

        self.selected_camera = StringVar()
        self.camera_cb = ttk.Combobox(self, textvariable=self.selected_camera)
        self.camera_cb.grid(padx=5, pady=5, row=1, column=2, columnspan=3)
        amount_of_cameras = camera_amount()
        self.camera_cb['values'] = [i for i in range(amount_of_cameras)]
        self.camera_cb.current(0)

        self.change_camera = Button(text="Change Camera", command=self.choose_camera)
        self.change_camera.grid(padx=5, pady=5, row=2, column=2, columnspan=3)

        self.freeze_cam = Button(text="Freeze camera", command=self.freeze_camera)
        self.freeze_cam.grid(padx=5, pady=5, row=3, column=2, columnspan=3)

        self.default_points = Button(text="Set default points", command=self.set_default_points)
        self.default_points.grid(padx=5, pady=5, row=4, column=2, columnspan=3)

        self.transform_button = Button(text="Transform", command=self.transform)
        self.transform_button.grid(padx=5, pady=5, row=5, column=2, columnspan=3)

        self.clear_button = Button(text="CLEAR", command=self.clear)
        self.clear_button.grid(padx=5, pady=5, row=6, column=2, columnspan=3)

        self.save_button = Button(text="SAVE", command=self.save)
        self.save_button.grid(padx=5, pady=5, row=7, column=2, columnspan=3)

        self.up_butt = Button(text="↑", command=self.up)
        self.up_butt.grid(padx=5, pady=5, row=9, column=3)

        self.down_butt = Button(text="↓", command=self.down)
        self.down_butt.grid(padx=5, pady=5, row=10, column=3)

        self.left_butt = Button(text="←", command=self.left)
        self.left_butt.grid(padx=5, pady=5, row=10, column=2, columnspan=2)

        self.right_butt = Button(text="→", command=self.right)
        self.right_butt.grid(padx=5, pady=5, row=10, column=4)

        # ----- fields -----
        self.cap = cv2.VideoCapture(int(self.selected_camera.get()))
        self.imgtk = None
        self.after_id = None
        self.resized11 = None
        self.ipm_matrix = None

        self.input_points = []
        self.output_points = []

        self.ipm_matrixes = [] # list of saved ipm_matrixes which satisfied us

    def choose_camera(self) -> None:
        self.freeze_camera(display=False)
        self.cap = cv2.VideoCapture(int(self.selected_camera.get()))
        self.clear()

    def freeze_camera(self, display: bool = True) -> None:
        self.canvas1.after_cancel(self.after_id)
        if display:
            self.resized11 = cv2.cvtColor(self.resized11, cv2.COLOR_BGR2RGB)
            cv2.namedWindow("image")
            cv2.setMouseCallback("image", self.draw_circle)
            while True:
                cv2.imshow("image", self.resized11)
                if cv2.waitKey(1) & 0xFF == ord("q"):
                    break
            cv2.destroyAllWindows()

    def set_default_points(self) -> None:
        default_points = [[448, 609], [580,609], [580,741], [448,741]]
        for pts in default_points:
            cv2.circle(self.resized11, pts, 5, (0, 0, 255), -2)
        self.canvas1.itemconfig(self.org_image_container, image=self.resized11)

    def transform(self) -> None:
        if len(self.input_points) == 4 and len(self.output_points) == 4:
            ordered_pts = transform.order_points(np.array(self.input_points, dtype=np.float32))
            ordered_out_pts = transform.order_points(np.array(self.output_points, dtype=np.float32))

            self.ipm_matrix = cv2.getPerspectiveTransform(ordered_pts, ordered_out_pts)
            self.transf_img = cv2.warpPerspective(self.resized11, self.ipm_matrix, self.resized11.shape[:2][::-1])

            self.transf_img = cv2.cvtColor(self.transf_img, cv2.COLOR_BGR2RGB)
            self.transf_img = Image.fromarray(self.transf_img)
            warpedtk = ImageTk.PhotoImage(self.transf_img)
            self.canvas2.warpedtk = warpedtk
            self.canvas2.itemconfig(self.trf_image_container, image=warpedtk)
        else:
            tkinter.messagebox.showwarning("Warning", "Choose right points to transformation!")

    def clear(self) -> None:
        self.output_points = []
        self.input_points = []
        self.show_frame()

    def save(self) -> None:
        # add to self.ipm_matrixes tuple (camera number, ipm_matrix)
        self.ipm_matrixes.append((int(self.selected_camera.get()), self.ipm_matrix))
        answer = tkinter.messagebox.askyesno("Save", "Do you want to save your all ipm matrixes to file?")
        if answer:
            self.safe_to_file()
        pass

    def up(self) -> None:
        pass

    def down(self) -> None:
        pass

    def left(self) -> None:
        pass

    def right(self) -> None:
        pass

    def draw_circle(self, event, x, y, flags, param) -> None:
        if event == cv2.EVENT_LBUTTONDBLCLK:
            cv2.circle(self.resized11, (x, y), 5, (255, 0, 0), -2)
            self.input_points.append([x, y])
        if event == cv2.EVENT_RBUTTONDBLCLK:
            cv2.circle(self.resized11, (x, y), 5, (0, 0, 255), -2)
            self.output_points.append([x, y])

    def show_frame(self) -> None:
        """
        https://www.tutorialspoint.com/how-to-show-webcam-in-tkinter-window
        """
        img11 = self.cap.read()[1]
        cv2image = cv2.cvtColor(img11, cv2.COLOR_BGR2RGB)
        self.resized11 = cv2.resize(cv2image, (canvas_width, canvas_height))
        im11 = Image.fromarray(self.resized11)
        self.imgtk = ImageTk.PhotoImage(im11)
        self.canvas1.imgtk = self.imgtk
        self.canvas1.itemconfig(self.org_image_container, image=self.imgtk)
        self.after_id = self.canvas1.after(10, self.show_frame)

    def safe_to_file(self) -> None:
        with open("saved_conf.txt", "a") as file:
            res = ""
            for elem in self.ipm_matrixes:
                res += f"Camera {elem[0]}: {elem[1]}\n"
            res += "\n\n"
            file.write(res)





if __name__ == "__main__":
    app = App()
    app.show_frame()
    app.mainloop()

但是如果有人有不同的想法,我怎么能在一个单一的 tkinter 中做到这一点 window 我将不胜感激。

如有任何帮助,我将不胜感激。

迈克尔

在 show_frame() 中,您使用 opencv 读取帧作为 Mat,然后使用 self.imgtk = ImageTk.PhotoImage(im11) 将其转换为 PIL 图像。 然后当您调用 set_default_points() 以使用 cv2.circle(self.imgtk, ... ) 时,您再次需要 Mat。

编辑: 您需要使用opencv格式(Mat)进行opencv操作,然后当您要显示结果时,Tkinter需要PIL格式。

opencv_image --> opencv_operation --> ImageTk.PhotoImage() --> Tkinter 显示