讀取多個(海康\大華)網絡攝像頭的視頻流 (使用opencv-python),解決實時讀取延遲問題


實時讀取視頻流(封面使用五個攝像頭是因為我手頭最多只有五個),解決實時讀取延遲卡頓問題

做計算機視覺的算法開發,可能會碰到實時獲取圖像並處理的問題,我寫了一個簡單的實例,可以實時讀取多個網絡攝像頭。運行視頻預覽如下↓ (可以看到視頻播放流暢,達到30fps,同時延遲小於0.3s)

實時讀取多個網絡攝像頭
 
2018-06-17 初版 Yonv1943 2018-06-25 小修改,deamon,setattr(), if is_opened
2018-07-02 添加單攝像頭run(): # single camera,和多攝像頭run_multi_camera()
2018-11-21 單線程讀取單個攝像頭,多進程讀取多個攝像頭
2019-02-14 將“多線程”改正為“多進程”謝謝   的糾正
2019-05-04 增加大華攝像頭rtsp協議 我在標題里不小心用愛發電了,為了公平起見,我把海康競爭對手大華也加上去。網絡攝像頭基本都支持rtsp協議,所以可以用本方法讀取。
2019-07-04 增加將多個攝像頭的畫面有序地讀取到同一個程序 run_multi_camera_in_a_window()
2019-09-06 回復評論:目標檢測
2019-10-17 回復評論:圖片剛放進去隊列就取出來,隊列里面豈不是沒有圖片?
2019-10-31 回復評論:網絡卡頓而無法獲取視頻信息,這個應該怎么處理?
2019-11-13 回復評論:為何有 ipv4 與ipv6兩種地址?

完整版Demo:

實現上述功能的完整示例Demo (已經盡可能短),你也可以在我的GitHub上下載到最新的代碼,如果星星多,那么我還會繼續加功能:

import cv2 import time import multiprocessing as mp """ Source: Yonv1943 2018-06-17 https://github.com/Yonv1943/Python/tree/master/Demo """ def image_put(q, name, pwd, ip, channel=1): cap = cv2.VideoCapture("rtsp://%s:%s@%s//Streaming/Channels/%d" % (name, pwd, ip, channel)) if cap.isOpened(): print('HIKVISION') else: cap = cv2.VideoCapture("rtsp://%s:%s@%s/cam/realmonitor?channel=%d&subtype=0" % (name, pwd, ip, channel)) print('DaHua') while True: q.put(cap.read()[1]) q.get() if q.qsize() > 1 else time.sleep(0.01) def image_get(q, window_name): cv2.namedWindow(window_name, flags=cv2.WINDOW_FREERATIO) while True: frame = q.get() cv2.imshow(window_name, frame) cv2.waitKey(1) def run_multi_camera(): # user_name, user_pwd = "admin", "password" user_name, user_pwd = "admin", "admin123456" camera_ip_l = [ "172.20.114.26", # ipv4 "[fe80::3aaf:29ff:fed3:d260]", # ipv6 # 把你的攝像頭的地址放到這里,如果是ipv6,那么需要加一個中括號。 ] mp.set_start_method(method='spawn') # init queues = [mp.Queue(maxsize=4) for _ in camera_ip_l] processes = [] for queue, camera_ip in zip(queues, camera_ip_l): processes.append(mp.Process(target=image_put, args=(queue, user_name, user_pwd, camera_ip))) processes.append(mp.Process(target=image_get, args=(queue, camera_ip))) for process in processes: process.daemon = True process.start() for process in processes: process.join() if __name__ == '__main__': run_multi_camera()

解決實時讀取延遲卡頓的關鍵代碼如下,我使用Python自帶的多線程隊列:

import multiprocessing as mp ... img_queues = [mp.Queue(maxsize=2) for _ in camera_ip_l] # queue ... q.put(frame) if is_opened else None # 線程A不僅將圖片放入隊列 q.get() if q.qsize() > 1 else time.sleep(0.01) # 線程A還負責移除隊列中的舊圖 ...

如果你可以成功運行上們的代碼,那么恭喜。如果你已經理解代碼,那么你不需要看下面的內容。


如果你想要進一步理解代碼,那么下面的內容是:

OpenCV官網提供的簡單版Demo(無法避免延遲卡頓)

 

簡單版Demo:

如果你可以成功運行上們的代碼,那么恭喜OpenCV官網給出的視頻流讀取示例代碼,經過簡單修改,如下:

def run_opencv_camera():
    video_stream_path = 0  # local camera (e.g. the front camera of laptop)
    cap = cv2.VideoCapture(video_stream_path)

    while cap.isOpened():
        is_opened, frame = cap.read()
        cv2.imshow('frame', frame)
        cv2.waitKey(1)
    cap.release()

當 video_stream_path = 0 的時候,電腦會開啟默認攝像頭,比如筆記本電腦的前置攝像頭 ↓

做網絡安全的人最喜歡貼掉的的前置攝像頭 QAQ,看到上面的白色小燈了嗎?

當我們需要讀取網絡攝像頭的時候,我們可以對 cap = cv2.VideoCapture(括號里面的東西進行修改),填寫上我們想要讀取的視頻流,它可以是:

  • 數字0,代表計算機的默認攝像頭(例如上面提及的筆記本前置攝像頭)
  • video.avi 視頻文件的路徑,支持其他格式的視頻文件
  • rtsp路徑(不同品牌的路徑一般是不同的,如下面舉出的海康與大華,詳細情況查看 附錄的「關於rtsp協議」)
user, pwd, ip, channel = "admin", "admin123456", "172.20.114.26", 1 video_stream_path = 0 # local camera (e.g. the front camera of laptop) video_stream_path = 'video.avi' # the path of video file video_stream_path = "rtsp://%s:%s@%s/h265/ch%s/main/av_stream" % (user, pwd, ip, channel) # HIKIVISION old version 2015 video_stream_path = "rtsp://%s:%s@%s//Streaming/Channels/%d" % (user, pwd, ip, channel) # HIKIVISION new version 2017 video_stream_path = "rtsp://%s:%s@%s/cam/realmonitor?channel=%d&subtype=0" % (user, pwd, ip, channel) # dahua cap = cv2.VideoCapture(video_stream_path)

直接使用參考官網寫出來的簡單版Demo有延遲卡頓問題,如果讀取速度低於視頻流的輸出速度,窗口顯示的圖片是好幾秒鍾前的內容。一段時間過后,緩存區將會爆滿,程序報錯,我可以使用rtsp讀取攝像頭:

def run_opencv_camera(): video_stream_path = 0 # local camera (e.g. the front camera of laptop) cap = cv2.VideoCapture(video_stream_path) while cap.isOpened(): is_opened, frame = cap.read() cv2.imshow('frame', frame) cv2.waitKey(1000) # wait for 1000ms(1s) HERE!!!!!!!!!!!!!! cap.release() """ 將等待時間修改為1秒,則攝像頭顯示的畫面延遲增大: 本地攝像頭可能不會報錯 網絡攝像頭可能會在運行一段時間后報錯  ERROR 報錯內容如下:(其實,如果傳遞給cv.imshow()函數的不是 ndarray,都會出現這個錯誤) libpng warning: iCCP: known incorrect sRGB profile libpng warning: iCCP: known incorrect sRGB profile libpng warning: iCCP: cHRM chunk does not match sRGB libpng warning: iCCP: known incorrect sRGB profile ... """

下面使用使用多線程隊列,解決這個延遲卡頓問題。

完整版Demo(使用多線程隊列,解決延遲卡頓問題,讀取多個攝像頭):

def image_put(q, user, pwd, ip, channel=1): cap = cv2.VideoCapture("rtsp://%s:%s@%s//Streaming/Channels/%d" % (user, pwd, ip, channel)) if cap.isOpened(): print('HIKVISION') else: cap = cv2.VideoCapture("rtsp://%s:%s@%s/cam/realmonitor?channel=%d&subtype=0" % (user, pwd, ip, channel)) print('DaHua') while True: q.put(cap.read()[1]) q.get() if q.qsize() > 1 else time.sleep(0.01) def image_get(q, window_name): cv2.namedWindow(window_name, flags=cv2.WINDOW_FREERATIO) while True: frame = q.get() cv2.imshow(window_name, frame) cv2.waitKey(1) def run_single_camera(): user_name, user_pwd, camera_ip = "admin", "admin123456", "172.20.114.26" mp.set_start_method(method='spawn') # init queue = mp.Queue(maxsize=2) processes = [mp.Process(target=image_put, args=(queue, user_name, user_pwd, camera_ip)), mp.Process(target=image_get, args=(queue, camera_ip))] [process.start() for process in processes] [process.join() for process in processes] def run_multi_camera(): # user_name, user_pwd = "admin", "password" user_name, user_pwd = "admin", "admin123456" camera_ip_l = [ "172.20.114.26", # ipv4 "[fe80::3aaf:29ff:fed3:d260]", # ipv6 ] mp.set_start_method(method='spawn') # init queues = [mp.Queue(maxsize=4) for _ in camera_ip_l] processes = [] for queue, camera_ip in zip(queues, camera_ip_l): processes.append(mp.Process(target=image_put, args=(queue, user_name, user_pwd, camera_ip))) processes.append(mp.Process(target=image_get, args=(queue, camera_ip))) for process in processes: process.daemon = True process.start() for process in processes: process.join() if __name__ == '__main__': # run_single_camera() run_multi_camera() pass 

關鍵部分解釋:我使用Python3自帶的多線程模塊,創建一個隊列,線程A從通過rtsp協議從視頻流中讀取出每一幀,並放入隊列中,線程B從隊列中將圖片取出,處理后進行顯示。線程A如果發現隊列里有兩張圖片(證明線程B的讀取速度跟不上線程A),那么線程A主動將隊列里面的舊圖片刪掉,換上新圖片。通過多線程的方法:

  • 線程A的讀取速度始終不收線程B的影響,防止網絡攝像頭的緩存區爆滿
  • 線程A更新了隊列中的圖片,使線程B始終讀取到最新的畫面,降低了延遲
import multiprocessing as mp ... img_queues = [mp.Queue(maxsize=2) for _ in camera_ip_l] # queue ... q.put(frame) if is_opened else None # 線程A不僅將圖片放入隊列 q.get() if q.qsize() > 1 else time.sleep(0.01) # 線程A還負責移除隊列中的舊圖 ...

模擬實時圖片處理

完整版代碼可以使用多線程隊列,解決延遲卡頓問題,並讀取多個攝像頭。我們把等待時間從1毫秒,增加到1秒(1000ms),模擬實時處理圖片中的某一個耗時操作。

cv2.imshow(window_name, frame) cv2.waitKey(1000) # 1000ms
模擬耗時操作
 

注:此處的視頻與文章開頭的視頻不同,因為左下角攝像頭模擬了耗時1秒的圖片處理延時操作,模擬實時圖片傳入處理速度慢的函數后 的情況。與此同時左上角的視頻作為對照。

可以看到,左上角是正常讀取窗口,20fps,延遲0.4秒。而左下角的模擬延遲視頻顯示窗口變為1fps,但是延遲沒有變化,依然是0.4秒,可以說:做到了讀取實時圖片的效果。所有代碼都可以從我的GitHub下載到完整注釋版


附錄是對評論區的回復,集中了常見問題。

關於rtsp協議:

通過rtsp協議讀取視頻流: 

 下面依次是我在網絡上查到的海康與大華 rtsp 讀取路徑。經過測試,我手頭的海康攝像頭支持前面兩種讀取方式(新舊兩種)。大華攝像頭用第三種讀取方式。

 

video_stream_path = "rtsp://%s:%s@%s/h264/ch%s/main/av_stream" % (user, pwd, ip, channel) # HIKIVISION old version 2015 video_stream_path = "rtsp://%s:%s@%s//Streaming/Channels/%d" % (user, pwd, ip, channel) # HIKIVISION new version 2017 video_stream_path = "rtsp://%s:%s@%s/cam/realmonitor?channel=%d&subtype=0" % (user, pwd, ip, channel) # dahua
海康、大華IpCamera RTSP地址和格式(原創,舊版)- 2014年08月12日 23:01:18 xiejiashu
最新(2017)海康攝像機、NVR、流媒體服務器、回放取流RTSP地址規則說明 - 2017年05月13日 xiejiashu

我在Win10、Ubuntu16 系統上,可以直接使用使用rtsp協議讀取網絡攝像頭視頻流。如果你碰到問題,那么你可能需要安裝XviD 與 FFmpeg ,如下:

Ubuntu16.04下安裝FFmpeg超簡單版sudo add-apt-repository ppa:djcj/hybrid # 添加源 sudo apt-get update # 更新源 sudo apt-get install ffmpeg # 安裝源 XviD-1.3.5XviD is an MPEG-4 compliant video CODEC.http://www.linuxfromscratch.org/blfs/view/svn/multimedia/xvid.html Download (HTTP): http://downloads.xvid.org/downloads/xvidcore-1.3.5.tar.gz

我用過的攝像頭主要有(都支持rtsp協議) :

  • 海康人臉攝像頭 XXX
  • 海康星光夜視XXX
  • 海康DS-IPC-B12
  • 大華雲台DH-PTZ12203UE-GN-P
  •  一般的,搜索的時候帶上rtsp就可以了

關於讀取卡頓:

我已經在很多攝像頭上面實驗過了,本文使用的代碼的性能如下:

環境:局域網 + 6個不同的POE供電海康攝像頭

  • 工控機(賽揚1.9G*4),CPU Celeron(R) J1900 ,4個攝像頭,1080P,最慢的延遲1.2秒
  • 筆記本(i3移動版 2.4G*4),CPU i3-3110M ,6個攝像頭,1080P,最慢的延遲 0.4秒

備注:如果出現卡頓,請檢查 網絡使用率,CPU使用率。

特別備注:如果你只有一個攝像頭,為了測試多個攝像頭的讀取效果,你開啟了多個讀取窗口同時讀取同一個攝像頭,你會發現:“對於一般的攝像頭,開啟兩個以上就會卡頓”,而這樣操作是不對的。因為攝像頭本身也是一個“服務器”,它無法為多個目標傳輸視頻流,會遇到傳輸瓶頸。

這段代碼已經在多種環境下,在多個不同的攝像頭上面測試過了,我認為它是可靠的。如果出問題,請在評論留言,不要私信我。我會抽時間把這篇文章以及代碼整理一下的。

Python的列表解析:

使用列表解析,可以縮短代碼,但是對於列表解析是否提高了Python代碼的可讀性,仍然是有爭議的。

processes = [] for queue, camera_ip in zip(queues, camera_ip_l): processes.append(mp.Process(target=image_put, args=(queue, user_name, user_pwd, camera_ip))) processes.append(mp.Process(target=image_get, args=(queue, camera_ip))) for process in processes: process.daemon = True process.start() for process in processes: process.join() # 上面的代碼,我完全可以寫成下面的形式,這是Python的列表解析功能 processes = [mp.Process(target=queue_img_put, args=(queue, user_name, user_pwd, camera_ip)), mp.Process(target=queue_img_get, args=(queue, camera_ip))] [setattr(process, "daemon", True) for process in processes] # process.daemon = True # 設置進程守護 [process.start() for process in process_l] [process.join() for process in process_l]

有序地收集多個攝像頭拍攝的畫面,並顯示出來 (回復

 等人)

本文的封面圖片采用了一個主進程打開了了5個子進程,每個子進程負責一個攝像頭實時畫面的讀取與顯示。

如果你需要收集多個攝像頭拍攝的畫面(也就是將5個子進程拍攝到的圖片收集 (collect) 到一個進程中去),那么我們需要這樣子處理:(將前文出現過的兩個函數稍作修改,即可得到,代碼同步更新到Github,覺得有用就給星星吧

run_multi_camera_in_a_window() 修改自 run_multi_camera() image_collect() 修改自 image_get() def image_collect(queue_list, camera_ip_l): import numpy as np # 實際使用的時候記得放在外面 """show in single opencv-imshow window""" window_name = "%s_and_so_no" % camera_ip_l[0] cv2.namedWindow(window_name, flags=cv2.WINDOW_FREERATIO) while True: imgs = [q.get() for q in queue_list] imgs = np.concatenate(imgs, axis=1) cv2.imshow(window_name, imgs) cv2.waitKey(1) # """show in multiple opencv-imshow windows""" # [cv2.namedWindow(window_name, flags=cv2.WINDOW_FREERATIO) # for window_name in camera_ip_l] # while True: # for window_name, q in zip(camera_ip_l, queue_list): # cv2.imshow(window_name, q.get()) # cv2.waitKey(1) def run_multi_camera_in_a_window(): user_name, user_pwd = "admin", "admin123456" camera_ip_l = [ "172.20.114.196", # ipv4 "[fe80::3aaf:29ff:fed3:d260]", # ipv6 # 我在這里分別用ipv4 與ipv6 打開了兩個攝像頭,只有一個攝像頭的話就填寫一個IP ] mp.set_start_method(method='spawn') # init queues = [mp.Queue(maxsize=4) for _ in camera_ip_l] processes = [mp.Process(target=image_collect, args=(queues, camera_ip_l))] for queue, camera_ip in zip(queues, camera_ip_l): processes.append(mp.Process(target=image_put, args=(queue, user_name, user_pwd, camera_ip))) for process in processes: process.daemon = True # setattr(process, 'deamon', True) process.start() for process in processes: process.join() 

實現效果截圖:把兩個攝像頭收集到的實時畫面傳給同一個進程:

合並為一張圖片,以便於顯示在同一個cv2.imshow() 窗口內(這里畫面模糊是因為鏡頭距離成像物體太近了)

注釋部分的實現效果截圖:(打開多個OpenCV imshow 窗口,且畫面有序顯示,不混淆)

把兩個攝像頭收集到的實時畫面傳給同一個進程,再由不同窗口顯示出來

本來以為這個實現是很簡單了,沒想到已經有超過3個人私信問過相同的問題了,所以我寫下這部分的內容。因為覺得沒有技術含量,不足以寫入正文,因此放在了文末的附錄內。

視頻讀取已經結束, 而程序沒有自動退出 @風荷一一(這個問題不太值得回復)

問題描述 @風荷一一:“將rtsp換成本地視頻后,當視頻結束后會出現報錯。cpp:352: error: (-215:Assertion failed),且繼續占用CPU”

cap = cv2.VideoCapture(video_stream_path) is_opened = cap.isOpened() while is_opened: # 如果視頻讀取完畢,那么 is_opened 為False,循環自動跳出 is_opened, frame = cap.read() cv2.imshow('frame', frame) cv2.waitKey(1) cap.release()

在顯示之前做一個目標檢測,怎么實現? 

曾伊言:實時視頻傳入深度學習目標檢測模型進行檢測​zhuanlan.zhihu.com圖標

把讀取模塊與計算模塊分開,詳細內容請看上面的文章「實時視頻傳入深度學習目標檢測模型進行檢測」:

很明顯,單線程是低效率的,CPU工作的時候,GPU在圍觀,反之亦然:
線程0: CPU讀取圖片→ GPU處理圖片(如:目標檢測)→ ... ... 

使用多線程的一個簡單高效的方案:
線程0: CPU讀取圖片↘  CPU讀取圖片↘ ... ...
線程1:        GPU處理圖片    GPU處理圖片 ... ...

剛剛放進去一張圖片,然后馬上取出來,隊列里面豈不是一張圖片也沒有? (雖然你后面似乎知道答案了,自己把評論刪除掉,但是問這個問題的人太多了)

程序其實是這樣運作的:image_put 與 image_get 是同時運行的。image_put 放入圖片后,如果沒有 image_get 把圖片取走,那么 image_put 就自己把圖片取走,實時替換成新的圖片。

def image_put(q, user, pwd, ip, channel=1): ... while True: q.put(cap.read()[1]) # 剛剛放進去一張圖片 q.get() if q.qsize() > 1 else time.sleep(0.01) # 然后馬上取出來 def image_get(q, window_name): ... while True: frame = q.get() # 等待隊列放入圖片,如果隊列里面沒有圖片,那么它會「阻塞」在這里 cv2.imshow(window_name, frame) cv2.waitKey(1)

當 image_get 運行到 q.get() 處,如果隊列里面沒有圖片,那么它會在這里等,直到 image_put 把圖片放入隊列,它把這張圖片取走后才繼續往下運行。由於image_get 一直在隊列處等候,因此它總是可以在image_put 把圖片刪除前 搶先把圖片取走,這是這個程序做到實時更新並且減少延遲的基本方法。

網絡卡頓而無法獲取視頻信息,應該處理? 

這篇文章解決的是:由“處理圖片的速度”慢於“攝像頭拍攝產生實時圖像的速度”所導致的延遲。而不是 由網絡條件不好導致“接收圖片的速度”過慢導致的延遲。解決方法:

  • 減少視頻流大小:降低幀率、減小畫幅、降低碼流、主碼流→輔碼流、H264→H265等
  • 清空攝像頭緩存:刷新與攝像頭的連接,重新運行 cap = cv2.VideoCapture(***)

問題分析:工作中的攝像頭會把未被接受的視頻流保存在自己的緩存里,如果緩存滿了它就會報錯(接收端會有xxxxxxxx sRGB xxxx 之類的報錯)。只要清空攝像頭緩存,就能解決這個問題,因此我們可以刷新與網絡攝像頭的連接 來掩蓋這個問題。

能夠應對網絡延遲的視頻流協議應該是:RTMP、WebRTC 之類的協議。而RTSP不在此列。

 

 

關於私信:

不建議通過私信與我進行交流,有問題請寫在評論區。它的好處:常見的問題可以被所有人看到;節省時間;避免重復回答。

 

在評論區指出的問題,我會修改到正文中,並注明貢獻者的名字。

在評論區提出的問題,我可能會嘗試解答,並添加到正文中。

交流是促進社區與自身成長的重要途徑,歡迎評論,謝謝大家。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM