【Python】改善 VideoCapture 的影像延遲

許多的範例程式大多僅介紹該如何用 VideoCapture 擷取攝影機的畫面,卻沒有充分說明其隱含的問題。
以下示範一個最基本的影像擷取程式。

# -*- coding: utf-8 -*-
import cv2

# ip camera 的擷取路徑
URL = "rtsp://admin:admin@192.168.1.1/video.h264"

# 建立 VideoCapture 物件
ipcam = cv2.VideoCapture(URL)

# 使用無窮迴圈擷取影像,直到按下Esc鍵結束
while True:
    # 使用 read 方法取回影像
    stat, I = ipcam.read()

    # 加上一些影像處理...

    # imshow 和 waitkey 需搭配使用才能展示影像
    cv2.imshow('Image', I)
    if cv2.waitKey(1) == 27:
        ipcam.release()
        cv2.destroyAllWindows()
        break

一般而言,都是這樣寫的。
先取回一幀影像,然後進行處理,膨脹閉合之類的,再使用CNN來辨識一下...等等。

全部都處理完了,再繼續截取下一幀影像。

這種範例程式占了90%的google版面。
當然了,那個趴數是夏恩胡謅的,這邊只是想表示 "很多" 的意思。

這種寫法有什麼問題呢?
其實只要稍微改動一下上述的程式,就可以看出來。
我們把 cv2.waitkey(1) 改成 cv2.waitkey(1000),意思是程式到這邊等待 1 秒。

在實務上的情況,我們不會直接使用waitkey(1000),
而是每一次迴圈內影像處理的流程費時 1 秒,也許會快一些或是慢一些。

然後我們就會發現一件事:怎麼影像不動了?或是影像怎麼會延遲?

尤其是影像上面如果有日期時間的話,就更明顯的看到秒數連動都不會動。
明明已經過了10秒鐘,取回10張影像,但是影像顯示的時間卻沒有任何改變?

原因是因為VideoCapture會把從攝影機取回來的影像先放到緩衝區,等待使用者將緩衝區內的影像取走,再填充新的影像進去。

如果攝影機的拍攝頻率是一秒10幀影像(10fps),但我們一秒只讀取一張,
那就表示我們會一直讀取到同一時刻的影像,直到把緩衝區清空為止,
又緩衝區有多少幀影像,則是取決於攝影機設定的拍攝頻率。

也因此在不明所以的人眼中看起來的問題就是:
為什麼怎麼我的影像串流會出現延遲的問題?

而這個問題最根本的原因是:

從緩衝區取出影像的速度,低於填入影像的速度!

要解決這個問題的方法有兩個:

一、降低攝影機的拍攝速度

攝影機都可以手動調整擷取影像的頻率,既然程式無法消耗這麼多幀影像,那就把攝影機的頻率降低。


那如果是即時影像辨識,非要這麼快的速度不可,
例如在門口的人臉辨識系統,速度太慢的可能會被主管電到飛上天。

這時候就可以考慮使用多執行緒的技巧。

二、多執行緒

將擷取影像的迴圈單獨放進一個執行緒,使其不斷地清空緩衝區,保留最新的影像。
另外在主程式的部分就是有需要的時候再將最新的影像取回來。

所以把上面的那支程式改成以下這樣:

# -*- coding: utf-8 -*-
import cv2
import time
import threading

# 接收攝影機串流影像,採用多執行緒的方式,降低緩衝區堆疊圖幀的問題。
class ipcamCapture:
    def __init__(self, URL):
        self.Frame = []
        self.status = False
        self.isstop = False
		
	# 攝影機連接。
        self.capture = cv2.VideoCapture(URL)

    def start(self):
	# 把程式放進子執行緒,daemon=True 表示該執行緒會隨著主執行緒關閉而關閉。
        print('ipcam started!')
        threading.Thread(target=self.queryframe, daemon=True, args=()).start()

    def stop(self):
	# 記得要設計停止無限迴圈的開關。
        self.isstop = True
        print('ipcam stopped!')
   
    def getframe(self):
	# 當有需要影像時,再回傳最新的影像。
        return self.Frame
        
    def queryframe(self):
        while (not self.isstop):
            self.status, self.Frame = self.capture.read()
        
        self.capture.release()

URL = "rtsp://admin:admin@192.168.1.1/video.h264"

# 連接攝影機
ipcam = ipcamCapture(URL)

# 啟動子執行緒
ipcam.start()

# 暫停1秒,確保影像已經填充
time.sleep(1)

# 使用無窮迴圈擷取影像,直到按下Esc鍵結束
while True:
    # 使用 getframe 取得最新的影像
    I = ipcam.getframe()
    
    cv2.imshow('Image', I)
    if cv2.waitKey(1000) == 27:
        cv2.destroyAllWindows()
        ipcam.stop()
        break

以上兩個建議,是夏恩覺得比較容易解決問題的方法。
若您也遇到相同的問題,請隨意取用。