zoukankan      html  css  js  c++  java
  • 读取多个(海康大华)网络摄像头的视频流 (使用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不在此列。

     

     

    关于私信:

    不建议通过私信与我进行交流,有问题请写在评论区。它的好处:常见的问题可以被所有人看到;节省时间;避免重复回答。

     

    在评论区指出的问题,我会修改到正文中,并注明贡献者的名字。

    在评论区提出的问题,我可能会尝试解答,并添加到正文中。

    交流是促进社区与自身成长的重要途径,欢迎评论,谢谢大家。

  • 相关阅读:
    eclipse中的Invalid text string (xxx).
    在jsp文件中出现Unknown tag (c:out)
    eclipse 界面复原
    ecilpse 纠错插件
    Multiple annotations found at this line:- The superclass "javax.servlet.http.HttpServlet" was not found on the Java Build Path
    Port 8080 required by Tomcat v9.0 Server at localhost is already in use. The server may already be running in another process, or a system process may be using the port.
    调用第三方https接口
    调用第三方http接口
    创建带值枚举
    spring整合redis之Redis配置文件
  • 原文地址:https://www.cnblogs.com/wangsongbai/p/13374170.html
Copyright © 2011-2022 走看看