最近因為工作需要,在ubuntu上開發了一個拍照程序。
為了找到合適的功能研究了好幾種實現方式,在這里記錄一下。
目錄
太長不看版
探索過程
v4l2
QT
opencv4.2
打開攝像頭
為什么不直接打開第一個視頻節點
獲取所有分辨率
切換攝像頭
太長不看版
技術:python3.8+opencv4.2+tkinter
支持的功能如下:
- 預覽
- 切換攝像頭
- 切換分辨率
- 拍照(點擊拍照之后,照片會顯示在右邊)
實現代碼在這里:
import tkinter as tk
import cv2
from PIL import Image, ImageTk
import tkinter.messagebox as messagebox
import sys
import os# Initialize window
root = tk.Tk()
root.title("UVC Camera")
root.geometry("1700x700")# Detect available cameras
camera_indexes = []
for i in range(10):cap = cv2.VideoCapture(i)if not cap.isOpened():continuecamera_indexes.append(i)cap.release()print("Available cameras:", camera_indexes)# Show error message if no camera is available
if len(camera_indexes) == 0:messagebox.showerror("Error", "Can't find the camera")sys.exit(0)# Show error message if camera cannot be opened
try:camera = cv2.VideoCapture(camera_indexes[0]) # Open the first detected camera by defaultcamera.set(6, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
except:messagebox.showerror("Error", "The camera won't open, the equipment is damaged or the contact is bad.")sys.exit(0)# Detect available resolutions
res_options = []
width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
res_options.append([width, height])for j in range(30):old_width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))old_height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))camera.set(cv2.CAP_PROP_FRAME_WIDTH, width+j*100)camera.set(cv2.CAP_PROP_FRAME_HEIGHT, height+j*100)new_width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))new_height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))if new_width != old_width:res_options.append([new_width, new_height])print("Available resolutions:", res_options)# Set the lowest resolution as the default
camera.set(cv2.CAP_PROP_FRAME_WIDTH, res_options[0][0])
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, res_options[0][1])# Button callback functionsdef on_capture():home_dir = os.path.expanduser('~')cv2.imwrite(home_dir + "/capture.png", img)# Resize the image while maintaining the aspect ratiocv2image = cv2.cvtColor(img, cv2.COLOR_BGR2RGBA)current_image = Image.fromarray(cv2image)w, h = current_image.sizeratio = min(850.0 / w, 638.0 / h)current_image = current_image.resize((int(ratio * w), int(ratio * h)), Image.ANTIALIAS)imgtk = ImageTk.PhotoImage(image=current_image)photo_panel.imgtk = imgtkphoto_panel.config(image=imgtk)messagebox.showinfo("Info", "Photo taken successfully")def on_switch_res(value):global cameracamera.set(cv2.CAP_PROP_FRAME_WIDTH, value[0])camera.set(cv2.CAP_PROP_FRAME_HEIGHT, value[1])def on_switch_cam(value):global camera# print("切換攝像頭")# print("選擇的值是: ", str(value))# 結束預覽root.after_cancel(video_loop_id)camera.release()# 創建新的捕捉對象并打開攝像頭camera = cv2.VideoCapture(value)camera.set(6, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) if not camera.isOpened():messagebox.showerror("Error", "The camera cannot be turned on.")sys.exit()on_video_loop()def on_video_loop():global img,video_loop_idsuccess, img = camera.read() # 從攝像頭讀取照片if success:cv2.waitKey(10)cv2image = cv2.cvtColor(img, cv2.COLOR_BGR2RGBA) # 轉換顏色從BGR到RGBAcurrent_image = Image.fromarray(cv2image) # 將圖像轉換成Image對象# 等比縮放照片w,h = current_image.sizeratio = min(850.0/w, 600.0/h)current_image = current_image.resize((int(ratio * w), int(ratio * h)), Image.ANTIALIAS)imgtk = ImageTk.PhotoImage(image=current_image)video_panel.imgtk = imgtkvideo_panel.config(image=imgtk)video_loop_id = root.after(1, on_video_loop)video_panel = tk.Label(root)
photo_panel = tk.Label(root)video_panel.grid( # 左上居中對齊row=0, column=0, columnspan=4, padx=20, pady=20, sticky=tk.NW
)photo_panel.grid( # 右上居中對齊row=0, column=4, columnspan=2,sticky=tk.EW, padx=20, pady=20
)# 攝像頭標簽+下拉框
label3 = tk.Label(root, text="Select camera")
label3.grid(row=1, column=0, sticky="E", padx=10, pady=10)variable1 = tk.StringVar(root)
variable1.set(camera_indexes[0])
cam_dropdown = tk.OptionMenu(root, variable1, *camera_indexes, command=on_switch_cam)
cam_dropdown.grid(row=1, column=1, sticky="W", padx=10, pady=10)# 分辨率標簽+下拉框
label4 = tk.Label(root, text="Select resolution")
label4.grid(row=1, column=2, sticky="E", padx=10, pady=10)variable2 = tk.StringVar(root)
variable2.set(res_options[0])
res_dropdown = tk.OptionMenu(root, variable2, *res_options, command=on_switch_res)
res_dropdown.grid(row=1, column=3, sticky="W", padx=10, pady=10)# 拍照和退出按鈕
capture_button = tk.Button(root, text="Take a picture", command=on_capture)
capture_button.grid(row=1, column=4, padx=10, pady=10)exit_button = tk.Button(root, text="Quit", command=root.quit)
exit_button.grid(row=1, column=5, padx=10, pady=10)# 一些頁面設置
root.grid_columnconfigure(0, weight=1)
root.grid_columnconfigure(1, weight=1)
root.grid_columnconfigure(2, weight=1)
root.grid_columnconfigure(3, weight=1)
root.grid_columnconfigure(4, weight=2)
root.grid_columnconfigure(5, weight=2)
root.grid_rowconfigure(0, weight=13)
root.grid_rowconfigure(1, weight=1)on_video_loop()
root.mainloop()
探索過程
v4l2
一開始在網上找到的其實是拍照程序是v4l2的,純c接口。
不過這個相機需要預覽,v4l2接口雖然拍照正常但是沒法預覽,所以放棄了這套方案。
相關內容記錄在:V4L2 零基礎入門(一)——打開攝像頭和獲取攝像頭基本信息_v4l2攝像頭采集-CSDN博客
QT
查看資料發現QT有封裝攝像頭相關的接口,在qtcreator里可以直接找到。
這個demo的功能很齊全,拍照,錄像都有,不過有個致命問題,高分辨率的時候預覽卡的太厲害,簡直卡成ppt。
opencv4.2
為了解決預覽卡頓的問題,開始查找其他的方案,最終找到了Python調用opencv接口。
這套方案在高分辨率下的預覽也很流暢。
實現的代碼我放在一開頭啦,有問題歡迎評論區。
在這邊解釋一些實現的細節。
打開攝像頭
我這里是先打開前10個視頻節點,10是為了處理同時連接多個攝像頭的情況(一個攝像頭有1或者2個節點)。
10這個數是隨便選的,可以改成其他的數
循環前10個節點,看哪個節點能被打開,把能打開的序號存儲在數組里。
最后打開數組里存儲的第一個節點,并設置照片格式為mjpg。
# Detect available cameras
camera_indexes = []
for i in range(10):cap = cv2.VideoCapture(i)if not cap.isOpened():continuecamera_indexes.append(i)cap.release()print("Available cameras:", camera_indexes)# Show error message if no camera is available
if len(camera_indexes) == 0:messagebox.showerror("Error", "Can't find the camera")sys.exit(0)# Show error message if camera cannot be opened
try:camera = cv2.VideoCapture(camera_indexes[0]) # Open the first detected camera by defaultcamera.set(6, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
except:messagebox.showerror("Error", "The camera won't open, the equipment is damaged or the contact is bad.")sys.exit(0)
為什么不直接打開第一個視頻節點
這里解釋一下,為什么繞這么大彎,挨個找哪個節點能打開。
一般來說,直接打開第一個視頻節點一般都不會有問題。
#直接打開第一個視頻節點,代碼會是這種形式
camera = cv2.VideoCapture(0)
但是可能出現這樣一種情況,即先連接了兩個攝像頭,此時視頻設備的節點編號分別為1和2。
如果取下了視頻設備的節點編號為1攝像頭,再打開拍照程序,如果直接打開第一個節點會出現錯誤。
簡單畫的示意圖如下:
獲取所有分辨率
獲取分辨率的流程有點復雜,先是通過CAP_PROP_FRAME_WIDTH和CAP_PROP_FRAME_HEIGHT獲取最小的分辨率。
然后循環將當前已知的最大的分辨率的x和y分別+100,嘗試這個分辨率在攝像頭上能否設置成功。
如果設置成功,則記錄改分辨率,在這個分辨率的的x和y基礎上分別+100,重復這個過程。
我這里設置了循環30次,這個也是隨意設置的,大家算一下能循環到攝像頭的最大分辨率即可。
# Detect available resolutions
res_options = []
width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
res_options.append([width, height])for j in range(30):# 前兩行是獲取當前分辨率old_width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))old_height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))camera.set(cv2.CAP_PROP_FRAME_WIDTH, width+j*100)camera.set(cv2.CAP_PROP_FRAME_HEIGHT, height+j*100)new_width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))new_height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))# 如果出現了新的可以設置成功的分辨率,保存下來if new_width != old_width:res_options.append([new_width, new_height])print("Available resolutions:", res_options)
這里可能會有個問題,如果x和y分別+100的所有分辨率都不是攝像頭支持的怎么辦呢?
其實攝像頭設置分辨率是比較智能的,不需要完全匹配。
假如支持是分辨率是950*650,實際設置分辨率1000*700,這種差的不太遠的,攝像頭會自動識別成自己支持的分辨率。(這只是個例子,實際差多少之內可以識別,沒有詳細測過)
切換攝像頭
切換攝像頭需要先把當前的預覽停掉,釋放當前的攝像頭。
再重新打開攝像頭,設置圖片類型。
def on_switch_cam(value):global camera# print("切換攝像頭")# print("選擇的值是: ", str(value))# 結束預覽root.after_cancel(video_loop_id)camera.release()# 創建新的捕捉對象并打開攝像頭camera = cv2.VideoCapture(value)camera.set(6, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) if not camera.isOpened():messagebox.showerror("Error", "The camera cannot be turned on.")sys.exit()on_video_loop()# 預覽
def on_video_loop():global img,video_loop_idsuccess, img = camera.read() # 從攝像頭讀取照片if success:cv2.waitKey(10)cv2image = cv2.cvtColor(img, cv2.COLOR_BGR2RGBA) # 轉換顏色從BGR到RGBAcurrent_image = Image.fromarray(cv2image) # 將圖像轉換成Image對象# 等比縮放照片w,h = current_image.sizeratio = min(850.0/w, 600.0/h)current_image = current_image.resize((int(ratio * w), int(ratio * h)), Image.ANTIALIAS)imgtk = ImageTk.PhotoImage(image=current_image)video_panel.imgtk = imgtkvideo_panel.config(image=imgtk)video_loop_id = root.after(1, on_video_loop)