I'm trying to display a video, and using the examples/solutions to the question on how to play a video on Tkinter don't seemt to yeild smooth and fast rendering.
For example:
frame = self.vid_cap.read()
frame = cv2.resize(frame, (696, 486))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
self.latest_img_ = Image.fromarray(frame)
self.latest_img_ = ImageTk.PhotoImage(image=self.latest_img_)
self.canvas.create_image(0, 0, anchor="nw", image=self.latest_img_)
The above code works, but video is too slow, even when using something like self.canvas.after(1, self.video_loop_). After some benchmarking, it turned out that the following line:
self.latest_img_ = ImageTk.PhotoImage(image=self.latest_img_)
Takes 9ms ~ 100ms regardless when I run the code on my quad-core mac or 12-core rig, while the entire code above it (from capturing, and until converting the numpy array to an image) takes 0ms to 1ms only.
What could be a faster alternative to ImageTk.PhotoImage?
You should keep a reference of the canvas object and modify it instead of creating new objects every time. To do that you can first create a placeholder by passing image=None. Below is a minimum sample:
import tkinter as tk
import cv2
from PIL import Image, ImageTk
class GUI(tk.Tk):
def __init__(self):
super().__init__()
self.canvas = tk.Canvas(self)
self.canvas.pack(fill="both", expand=1)
self.streaming_image = self.canvas.create_image(0, 0, anchor="nw", image=None)
def show_frame(self):
_, frame = self.vid_cap.read()
frame = cv2.resize(frame, (696, 486))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = Image.fromarray(frame)
self.latest_img = ImageTk.PhotoImage(image=img)
self.canvas.itemconfig(self.streaming_image, image=self.latest_img)
self.after(10, self.show_frame)
def start_video(self, path=None):
self.vid_cap = cv2.VideoCapture(path)
self.show_frame()
root = GUI()
root.start_video("your_path_here")
root.mainloop()
Related
I found on the internet this python code for counting people with the Open CV library. I would need to open the window that opens with CV2, inside a Tkinter window in order to then add the commands for the settings.
This is the code from GitHub: https://github.com/Gupu25/PeopleCounter
How can I make the two OpenCv windows open inside a Tkinter window?
Here is a minimal example of opening a video in a tkinter window with OpenCV's VideoCapture object:
from tkinter import NW, Tk, Canvas, PhotoImage
import cv2
def photo_image(img):
h, w = img.shape[:2]
data = f'P6 {w} {h} 255 '.encode() + img[..., ::-1].tobytes()
return PhotoImage(width=w, height=h, data=data, format='PPM')
def update():
ret, img = cap.read()
if ret:
photo = photo_image(img)
canvas.create_image(0, 0, image=photo, anchor=NW)
canvas.image = photo
root.after(15, update)
root = Tk()
root.title("Video")
cap = cv2.VideoCapture("video.mp4")
canvas = Canvas(root, width=1200, height=700)
canvas.pack()
update()
root.mainloop()
cap.release()
To display 2 OpenCV videos, simply make a few adjustments, and use the np.hstack() method or the np.vstack() method, depending on whether you want your videos to be displayed side by side horizontally or vertically:
from tkinter import NW, Tk, Canvas, PhotoImage
import cv2
import numpy as np
def photo_image(img):
h, w = img.shape[:2]
data = f'P6 {w} {h} 255 '.encode() + img[..., ::-1].tobytes()
return PhotoImage(width=w, height=h, data=data, format='PPM')
def update():
ret1, img1 = cap1.read()
ret2, img2 = cap2.read()
if ret1:
photo = photo_image(np.hstack((img1, img2)))
canvas.create_image(0, 0, image=photo, anchor=NW)
canvas.image = photo
root.after(15, update)
root = Tk()
root.title("Video")
cap1 = cv2.VideoCapture("video1.mp4")
cap2 = cv2.VideoCapture("video2.mp4")
canvas = Canvas(root, width=1200, height=700)
canvas.pack()
update()
root.mainloop()
cap.release()
I'm pretty new to python and espcially tkinter and opencv.
I've got someway (a little way) to creating a gui that will eventually control a microscope and ai. But I've hit a stumbling block, trying to record the video that is displayed within the gui, I think its to do with the video feed already been captured in the display, but I can't find a way around it. It all works fine until I hit record then it crashes and i get the error: open VIDEOIO(V4L2:/dev/video0): can't open camera by index.
Apologies for the long code but I've cut it down to as much as I think possible.
The problem is in the root.recbtn and def rec sections.
import cv2
import tkinter as tk
import multiprocessing
from tkinter import *
from PIL import Image,ImageTk
from datetime import datetime
from tkinter import messagebox, filedialog
e = multiprocessing.Event()
p = None
# Defining CreateWidgets() function to create necessary tkinter widgets
def createwidgets():
root.cameraLabel = Label(root, bg="gray25", borderwidth=3, relief="ridge")
root.cameraLabel.grid(row=2, column=1, padx=10, pady=10, columnspan=3)
root.browseButton = Button(root, bg="gray25", width=10, text="BROWSE", command=destBrowse)
root.browseButton.grid(row=1, column=1, padx=10, pady=10)
root.recbtn = Button(root, bg="gray25", width=10, text="Record", command=rec)
root.recbtn.grid(row=1, column=5, padx=10, pady=10)
root.saveLocationEntry = Entry(root, width=55, textvariable=destPath)
root.saveLocationEntry.grid(row=1, column=2, padx=10, pady=10)
# Calling ShowFeed() function
ShowFeed()
# Defining ShowFeed() function to display webcam feed in the cameraLabel;
def ShowFeed():
# t5 # Capturing frame by frame
ret, frame = root.cap.read()
if ret:
# Flipping the frame vertically
frame = cv2.flip(frame, 1)
# Changing the frame color from BGR to RGB
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
# Creating an image memory from the above frame exporting array interface
videoImg = Image.fromarray(cv2image)
# Creating object of PhotoImage() class to display the frame
imgtk = ImageTk.PhotoImage(image = videoImg)
# Configuring the label to display the frame
root.cameraLabel.configure(image=imgtk)
# Keeping a reference
root.cameraLabel.imgtk = imgtk
# Calling the function after 10 milliseconds
root.cameraLabel.after(10, ShowFeed)
else:
# Configuring the label to display the frame
root.cameraLabel.configure(image='')
def destBrowse():
# Presenting user with a pop-up for directory selection. initialdir argument is optional
# Retrieving the user-input destination directory and storing it in destinationDirectory
# Setting the initialdir argument is optional. SET IT TO YOUR DIRECTORY PATH
destDirectory = filedialog.askdirectory(initialdir="YOUR DIRECTORY PATH")
# Displaying the directory in the directory textbox
destPath.set(destDirectory)
def rec():
vid_name = datetime.now().strftime('%d-%m-%Y %H-%M-%S')
# If the user has selected the destination directory, then get the directory and save it in image_path
if destPath.get() != '':
vid_path = destPath.get()
# If the user has not selected any destination directory, then set the image_path to default directory
else:
messagebox.showerror("ERROR", "No Directory Selected!")
# Concatenating the image_path with image_name and with .jpg extension and saving it in imgName variable
vidName = vid_path + '/' + vid_name + ".avi"
capture = cv2.VideoCapture(0)
fourcc = cv2.VideoWriter_fourcc('X', 'V', 'I', 'D')
videoWriter = cv2.VideoWriter(vidName, fourcc, 30.0, (640, 480))
while (True):
ret, frame = capture.read()
if ret:
cv2.imshow('video', frame)
videoWriter.write(frame)
if cv2.waitKey(1) == 27:
break
capture.release()
videoWriter.release()
# Creating object of tk class
root = tk.Tk()
# Creating object of class VideoCapture with webcam index
root.cap = cv2.VideoCapture(0)
# Setting width and height
width, height = 1200, 1200
root.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
root.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
# Setting the title, window size, background color and disabling the resizing property
root.title("Test-AI-tes")
root.geometry("1600x1024")
root.resizable(True, True)
root.configure(background = "gray18")
# Creating tkinter variables
destPath = StringVar()
imagePath = StringVar()
createwidgets()
root.mainloop()
Thanks!
This answer is similar to #Art's answer but I removed the after_id and queue.
import cv2
import threading
import tkinter as tk
from PIL import Image, ImageTk
def stop_rec():
global running
running = False
start_button.config(state="normal")
stop_button.config(state="disabled")
def start_capture():
global capture, last_frame
capture = cv2.VideoCapture(0)
fourcc = cv2.VideoWriter_fourcc("X", "V", "I", "D")
video_writer = cv2.VideoWriter(r"sample.avi", fourcc, 30.0, (640, 480))
while running:
rect, frame = capture.read()
if rect:
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
last_frame = Image.fromarray(cv2image)
video_writer.write(frame)
capture.release()
video_writer.release()
def update_frame():
if last_frame is not None:
tk_img = ImageTk.PhotoImage(master=video_label, image=last_frame)
video_label.config(image=tk_img)
video_label.tk_img = tk_img
if running:
root.after(10, update_frame)
def start_rec():
global running
running = True
thread = threading.Thread(target=start_capture, daemon=True)
thread.start()
update_frame()
start_button.config(state="disabled")
stop_button.config(state="normal")
def closeWindow():
stop_rec()
root.destroy()
running = False
after_id = None
last_frame = None
root = tk.Tk()
root.protocol("WM_DELETE_WINDOW", closeWindow)
video_label = tk.Label()
video_label.pack(expand=True, fill="both")
start_button = tk.Button(text="Start", command=start_rec)
start_button.pack()
stop_button = tk.Button(text="Stop", command=stop_rec, state="disabled")
stop_button.pack()
root.mainloop()
It uses the boolean flag running instead of using after_id. Also instead of storing the images in a queue then showing it, I only keep the last image. That way it can run in real time on my computer. Don't worry all of the frames are still being stored in the video file.
You cannot have infinite while loop along with the GUI's loop. You should instead make use of threading, whenever you have an IO operation to complete.
Example code:
import cv2
import threading
import tkinter as tk
from PIL import Image, ImageTk
from queue import Queue
def stop_rec():
global running, after_id
running = False
if after_id:
root.after_cancel(after_id)
after_id = None
with frame_queue.mutex:
frame_queue.queue.clear()
def start_capture():
global capture
capture = cv2.VideoCapture(0)
fourcc = cv2.VideoWriter_fourcc('X', 'V', 'I', 'D')
video_writer = cv2.VideoWriter(r"sample.avi", fourcc, 30.0, (640, 480))
while running:
rect, frame = capture.read()
if rect:
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
videoImg = Image.fromarray(cv2image)
# current_frame = ImageTk.PhotoImage(image = videoImg)
frame_queue.put(videoImg)
video_writer.write(frame)
capture.release()
video_writer.release()
def update_frame():
global after_id
if not frame_queue.empty():
video_label.image_frame = ImageTk.PhotoImage(frame_queue.get_nowait())
video_label.config(image=video_label.image_frame)
after_id = root.after(10, update_frame)
def start_rec():
global running
stop_rec()
running = True
thread = threading.Thread(target=start_capture, daemon=True)
thread.start()
update_frame()
def closeWindow():
stop_rec()
root.destroy()
running = False
after_id = None
frame_queue = Queue()
root = tk.Tk()
root.protocol("WM_DELETE_WINDOW", closeWindow)
video_label = tk.Label()
video_label.pack(expand=True, fill="both")
tk.Button(text="Start", command=start_rec).pack()
tk.Button(text="stop", command=stop_rec).pack()
root.mainloop()
Quick explanation:
start the record function in a new thread.
Use Queue to store the frames.
Then make use of [widget].after to update the label at regular intervals.
To stop the recording make use of [widget].after_cancel(after_id)(after_id is returned when you use .after method) and set the running variable to False to stop the loop.
I am trying to build a GUI using tkinter in python 3.6.4 64-bit on Windows 8 by integrating opencv components into the program. I can get the video to play, but there is significant flicker going on. Namely, a screen the same color as the native tkinter background shows up briefly several times a second. I've tested several cameras with similar results, and double-checked that the cameras work properly via native video playback software. Here is my code:
from tkinter import *
from PIL import Image, ImageTk
import cv2
import threading
cap = cv2.VideoCapture(0)
root = Tk()
def videoLoop():
global root
global cap
vidLabel = None
while True:
ret, frame = cap.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = Image.fromarray(frame)
frame = ImageTk.PhotoImage(frame)
if vidLabel: vidLabel.configure(image=frame)
else:
vidLabel = Label(root, image=frame, anchor=NW)
vidLabel.pack(expand=YES, fill=BOTH)
videoThread = threading.Thread(target=videoLoop, args=())
videoThread.start()
root.mainloop()
Could anyone suggest what I might be doing wrong? I did hear that tkinter doesn't always play well with threads, but that's about all I can think of. In response to a comment suggesting the flicker is caused by label updating, I added some code that still reads from video and updates the label within the loop, but uses an image loaded outside of the loop to update the label. The flicker then goes away, although (to my understanding) the efficiency of the loop and updating of the label hasn't changed. Here is the changed videoLoop function (with flicker gone):
def videoLoop():
global root
vidLabel = None
cap = cv2.VideoCapture(0)
ret, frame = cap.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = Image.fromarray(frame)
frame = ImageTk.PhotoImage(frame)
while True:
ret, lame = cap.read()
lame = cv2.cvtColor(lame, cv2.COLOR_BGR2RGB)
lame = Image.fromarray(lame)
lame = ImageTk.PhotoImage(lame)
if vidLabel:
vidLabel.configure(image=None)
vidLabel.configure(image=frame)
else:
vidLabel = Label(root, image=frame, anchor=NW)
vidLabel.pack(expand=YES, fill=BOTH)
Solution: Make sure the image configure call precedes storing the image in the label image attribute.
I've managed to solve this issue, however I don't fully understand WHY it works as I am a python/tkinter novice. I will post the solution for now and will update the answer when I manage to find a proper explanation to this behavior. My best guess is that the storing of the image in the label attribute is what actually causes it to update on the screen, while the configure method just declares that there will be an image attached, which causes the loop to have to go through another iteration before getting to the update image update statement. The below code works fine without flicker:
from tkinter import *
from PIL import Image, ImageTk
import cv2
import threading
cap = cv2.VideoCapture(0)
root = Tk()
def videoLoop():
global root
global cap
vidLabel = Label(root, anchor=NW)
vidLabel.pack(expand=YES, fill=BOTH)
while True:
ret, frame = cap.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = Image.fromarray(frame)
frame = ImageTk.PhotoImage(frame)
vidLabel.configure(image=frame)
vidLabel.image = frame
videoThread = threading.Thread(target=videoLoop, args=())
videoThread.start()
root.mainloop()
In tkinter in order to display images, the image needs to have a global reference reference that doesn't go out of scope and then gets garbage-collected, perhaps flickering is caused by lack of such reference. See below code that has such reference to the image, and also ditched the if/else with better structuring:
from tkinter import *
from PIL import Image, ImageTk
import cv2
import threading
cap = cv2.VideoCapture(0)
root = Tk()
def videoLoop():
global root
global cap
vidLabel = Label(root, anchor=NW)
vidLabel.pack(expand=YES, fill=BOTH)
while True:
ret, frame = cap.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = Image.fromarray(frame)
vidLabel.image = ImageTk.PhotoImage(frame)
vidLabel.configure(image=vidLabel.image)
videoThread = threading.Thread(target=videoLoop, args=())
videoThread.start()
root.mainloop()
I have the following piece of code:
from tkinter import *
from tkinter import filedialog
from PIL import Image, ImageTk
import cv2
import os
import glob
import numpy as np
image_path = ""
image_list = []
class Proj:
def __init__(self, master):
self.master = master
#GUI height and width
w = 1250
h = 600
# open folder manager to select image folder
image_path = filedialog.askdirectory()
master.geometry("%dx%d%+d%+d" % (w ,h ,0,0))
master.resizable(True,True)
#read in images from folder
self.read_images(master, image_path)
#cv2.imshow('image',cv2.imread(image_list[0], 1))
self.img = cv2.imread(image_list[0])
self.img = Image.fromarray(np.array(self.img).copy())
self.img.thumbnail((w//2, w//2+10))
self.img = ImageTk.PhotoImage(self.img)
image_frame = Frame(master)
image_frame.grid(row = 0, column = 0, columnspan = 3, rowspan = 5)
left_panel = Canvas(image_frame, width=w//2, height=h-70)
left_panel.grid(row=0, column=0, columnspan=4)
imgA_handler = left_panel.create_image((0,0), image = self.img, anchor="nw")
right_panel = Canvas(image_frame, width=w//2, height=h-70)
right_panel.grid(row=0, column=5, columnspan=4)
def read_images(self, master, path):
images = path + '/*.tif'
for img in glob.glob(images): #will only read in tif images
image_list.append(img)
root = Tk()
example = Proj(root)
root.mainloop()
I am reading in color .tif images and then trying to display them in the left_panel. However, when I go to do that, it shows the normal color image only in red scale even though I never extracted just the red signal. I am completely unable to diagnose the issue. How do I fix this issue?
Ultimately, what I want to do is display two images on this GUI. An original image on the left and a modified image on the right. Currently my gui layout is set up as I've coded above. However, if you think there is an easier way to do this, then I'd be interested to hear.
This is because opencv uses BGR instead of RGB. When you are using this line: self.img = Image.fromarray(np.array(self.img).copy()) Blue and Red colors are being swapped. Just before you use the above code, convert the BGR to RGB in opencv and you should be good to go.
self.img = cv2.cvtColor(self.img,cv2.COLOR_BGR2RGB)
I see you are using opencv. Why don't you use the opencv command to resize.
It is very simple and works pretty well.
Take a look at this page:
OpenCV Resize
This is an example to make a image 50% smaller, or instead of the fx and fy, you can put the exact size you want.
thumbnail = cv2.resize(image, (0,0), fx=0.5, fy=0.5)
I'm trying to create a GUI that opens a video and an image below it:
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
import Image, ImageTk
import Tkinter, tkMessageBox
import ttk
import cv2
import sys
width, height = 800, 600
banner = cv2.imread('../data/banner.png')
b,g,r = cv2.split(banner)
banner = cv2.merge((r,g,b))
im = Image.fromarray(banner)
cap = cv2.VideoCapture('../data/sample.mov')
root = Tkinter.Tk()
root.bind('<Escape>', lambda e: root.quit())
root.title("Contador")
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
lmain = Tkinter.Label(root)
lmain.grid(row=0,column=0,sticky='nsew')
bmain = Tkinter.Label(root)
bmain.grid(row=1,column=0,sticky='nsew')
baner = ImageTk.PhotoImage(image=im)
bmain.configure(image=baner)
def show_frame():
_, frame = cap.read()
if frame is None:
return
# labelWidth = root.winfo_screenwidth()
# labelHeight = root.winfo_screenheight()
# maxsize = (labelWidth, labelHeight)
# frame = frame.resize(maxsize)
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(frame)
imgtk = ImageTk.PhotoImage(image=img)
lmain.imgtk = imgtk
lmain.configure(image=imgtk)
lmain.after(10, show_frame)
show_frame()
root.mainloop()
The problems I'm having are the following:
I need to resize the image to fit the label.
the commented part i got from here (how to fit image to label in Python) but it gives a channel number error(line 40) and further down the code gives a NoneType error(line 41) and a invalid type of image(numpy array) in line 42
the image and video don't change size when resizing the window
So I need solution for this tkinter code(or even a better framework for python)
I found out what the problem was.
Turns out the line I got from another question:
frame = frame.resize(maxsize)
should be:
frame = cv2.resize(frame, maxsize)
Also to get the label's size the command is:
labelWidth = lmain.winfo_width()
labelHeight = lmain.winfo_height()
since
labelWidth = root.winfo_width()
labelHeight = root.winfo_height()
gets the root's size not the label's.