美文网首页
Pyimagesearch CV系列之三:HTML获取OpenC

Pyimagesearch CV系列之三:HTML获取OpenC

作者: 吃远 | 来源:发表于2019-10-09 22:16 被阅读0次

一、概述

  通常我们可以使用opencv获取视频流进而对usb camera或者本地的视频进行可视化。那么我们能否获取远程电脑的视频流,通过某些算法进行处理之后可视化出来?比如说我们想调用房间里的摄像头,查看🐈有没有捣乱;或者调用无人小车🚙的前置摄像头,和其共享视野,这些场景就需要通过某种途径获取到远程电脑(即使没有X服务)的视频流,然后在自己的笔记本上显示出来了。

  这篇文章将记录一个简单的pipeline,实现视频的读取、处理,并将视频流定向到HTML中。本文将涉及以下知识点:

  • 复习基本的视频流获取
  • 安装并调用dlib库中的人脸检测(基于hog)和特征点检测API对视频进行处理
  • 通过Flask将视频流定向到HTML
  • 通过threading确保并发线程安全(进而支持多clients同时使用该视频流)
  • 通过Python生成器将获取的视频帧重新输出为“视频流”的形式
Fig. 1 效果展示:网页获取脚本中处理过的视频流

二、基本组件

  本章复习/学习一下本文涉及到的一些问题,有了这些基本组件,后面只需要做一些衔接的工作就可以了。

2.1 获取本地视频/webcam的视频流

  通常可以用OpenCV写个循环来获取、处理视频流。可以设置VideoCapture的参数为0或者视频文件路径来切换视频流的来源。

import cv2
cap = cv2.VideoCapture(0)
while(True):
    # capture frame-by-frame
    ret , frame = cap.read()
    
    #转换为灰度图
    gray = cv2.cvtColor(frame , cv2.COLOR_BGR2GRAY)
    # rgb = frame[...,::-1] # 转换为RGB

    # display the resulting frame
    cv2.imshow('frame',gray)
    if cv2.waitKey(1) &0xFF ==ord('q'):  #按q键退出
        break
# when everything done, release the capture
cap.release()
cv2.destroyAllWindows()

  再看下Adrian封装的imutils.video.VideoStream,如果用web camera的话其实和上面的代码没有明显区别。比较不同的可能是将视频流用一个Thread类对象封装起来,将逐帧获取的过程作为一个守护进程来看待。最后结束时直接用Thread.stop()方法结束这个进程。然后省去了cap.release和destroyAllWindow这两个操作。

  其实我并不理解这种通过Thread的做法有什么其他的考虑。这个回头如果遇到有类似的再深入了解吧。

Adrian的封装代码见下图:


Fig. 2 imutils中对cv2.VidewCapture的封装

  不过VideoStream主要的功能是提供了一个使用树莓派设备获取视频流的选项。看了下源码,这部分主要是调用了另一个比较大的开源库picamera。这个库里面的代码动不动三四千行...果然写驱动的都是真的大佬..随手看了下,这个docs相当人性化,还给了一些树莓派相关的Setup和贴心小提示,比如不要热插拔相机之类的,后面等我的Pi回来以后详细了解下~

Fig. 3 picamera模块给出了树莓派相机相关的setup

  言归正传,虽然imutils中的VideoStream和OpenCV的差别不大,这里还是试一试:

from imutils.video import VideoStream
import cv2

vs = VideoStream(src=0).start()
while True:
    frame = vs.read()
    cv2.imshow("figure", frame)
    if cv2.waitKey(1) &0xFF == ord('q'):
        break
vs.stop()

  顺利的起了web camera。然后最后虽然没有destroyAllWindow那两步操作,但是可能是通过Thread.stop把相关的进程结束了?视频窗口也可以顺利的关闭,未出现figure卡住未响应的情况。

2.2 安装、体验dlib库

  直接pip安装即可。

pip install dlib -i https://pypi.tuna.tsinghua.edu.cn/simple

  更详细的安装(Ubuntu/MacOS)可以参考Adrian的另一篇博客。值得注意的是dlib作为一个c++实现的计算机视觉开源模块,封装了很多快速的检测、识别类算法,非常适用于树莓派等边缘设备。

2.3 Flask

  Flask是一个轻量级的web开发库,其Python API语法较简介,以修饰器为基础,通过预先获取的templates对网页进行渲染。我在Flask Python API初探中记录了一些简单的hello world demo,有兴趣的同学可以了解下。

2.4 Threading

  Threading模块主要用于对多个线程之间的关系进行处理。值得注意的是Threading并不能提供真正的多线程,而是一种伪多线程。其所负责的工作是转交/切换不同线程之间的控制权,但是每个时刻只有一个线程是在工作的,即所有线程之间并不是并发的关系。真正的多线程需要参考multithreading模块。

  Threading模块主要的方法如下:

2.4.1 用于概览线程状况
  • threading.active_count
  • threading.enumerate
  • threading.current_thread
2.4.2 创建与启动线程
  • threading.Thread(target=func, args=(arg1, arg2)) #args也可以传入一个dict
  • t.start # t为通过Thread实例化的线程对象
  • t.stop
2.4.3 线程之间关系
  • t.join # 处理主线程和子线程之间的关系,表示要堵塞主线程直到这个线程完成,并不影响子线程的同时进行,只是代表在join()后边的语句必须等
  • lock#处理多个子线程之间的关系,语法如下:
lock.aquire()
cmd...
lock.release()

或者更简单的:

with lock:
  cmd...

  lock表示要阻止线程同时访问相同的共享数据来防止线程相互干扰,所以线程只能一个一个执行,不能同时进行。可以看下面的例子:

def job1():
    global A, lock
    with lock:
        for i in range(5):
            A += 1
            time.sleep(0.1)
            print('job1: ', A)

def job2():
    global A, lock
    lock.acquire()
    for i in range(5):
        A += 10
        time.sleep(0.1)
        print('job2: ', A)
    lock.release()


if __name__ == '__main__':
    lock = threading.Lock()
    A = 0
    t1 = threading.Thread(target=job1)
    t2 = threading.Thread(target=job2)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
job1:  1
job1:  2
job1:  3
job1:  4
job1:  5
job2:  15
job2:  25
job2:  35
job2:  45
job2:  55

2.5 生成器

  网上介绍生成器的博客非常多。这里仅记录如下几句话:


  1. 在 Python 中,使用了 yield 的函数被称为生成器(generator)。
  2. 跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作。调用一个生成器函数,返回的是一个迭代器对象。
  3. 在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。


  看一个简单例子复习下:


Fig. 4 生成器的一个例子

三、将OpenCV获取的视频流定向到HTML

   这一部分来实现用OpenCV获取视频流并经过dlib人脸检测模型的处理(只是一个例子,代表我们可以对视频流做任何处理之后放到网页上),将处理后的帧以byte流的形式发送给网页Response。该部分按照顺序记录脚本的内容。首先import相关模块:

from dlib_detector.face_model import FaceModel
from imutils.video import VideoStream
from flask import Response
from flask import Flask
from flask import render_template
import threading
import argparse
from datetime import datetime
import imutils
import time
import cv2

   这里的imutils是Adrian编写的一个helper func库,主要是在一些开源库的基础上做了轻量级封装提高使用的便捷性。threading用于线程管理;argparse用于解析命令行参数。

  接下来是一些setup工作:

# initialize the putput frame and a lock used to ensure thread-safe excahges of the output frames
outputFrame = None
lock = threading.Lock()

# initialize a flask object
app = Flask(__name__)

# initialize the video stream and allow the camera sensor to warmup

# for RPi camera:
# vs = VideoStream(usePiCamera=1).start()

# for usb camera:
vs = VideoStream(src=0).start()
time.sleep(2.0)

# next function will render template file: index.html and serve up the output video stream:
@app.route("/")
def index():
    # return the rendered template
    return render_template("index.html")

  这部分初始化一个Lock对象用于后面保证线程安全。注意如果使用树莓派的摄像头,参数需要进行更换。

  然后通过函数index使用templates文件夹下index.html模板文件对root url进行渲染。以下为index.html的内容:

<html>
  <head>
    <title>Landmarks Detection</title>
  </head>
  <body>
    <h1>Pi Video Surveillance</h1>
    <img src="{{ url_for('video_feed') }}">
  </body>
</html>

  注意倒数第三行给了一个参数入口(用{{ }}包住的就是参数),用来传递处理之后的视频流。

  下面一个函数定义我们apply到输入视频帧上面的算法:

def detect_landmarks(frameCount):
    # grab global references to the video stream, output frame,
    # and lock variables
    global vs, outputFrame, lock

    # TODO: initialize landmark model
    # model = FaceModel()
    model = FaceModel("dlib_detector/models/shape_predictor_68_face_landmarks.dat")

    # loop over frames from the video stream
    while True:
        # read the next frame, resize it, convert to grayscale and blur it
        frame = vs.read()
        frame = imutils.resize(frame, width=800)
        # TODO: apply related algorithm to captured frame.
        pred = model.predict(frame)

        # grab the current timestamp and draw it on the frame
        timestamp = datetime.now()
        cv2.putText(frame, timestamp.strftime(
            "%A %d %B %Y %I:%M:%S:%p"), (10, frame.shape[0]-10),
            cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0,0,255), 1)

        # TODO: visualize detection results

        # acquire the lock, set the output frame, and release the lock
        # We need to acquire the lock to ensure the outputFrame variable
        # is not accidentally being read by a client while we are trying
        # to update it.
        with lock:
            outputFrame = frame.copy()

  注意到函数最上面通过global关键字获取到三个全局变量(引用)。不严格的说,类似这句代码通常代表着通过lock来阻止多个线程对共享数据进行处理时发生相互干扰。但是这个例子中似乎没有涉及到不同线程对共享数据(outputFrame)的操作,Adrian说用线程锁是为了保证用户在打开不同tab或者不同用户同时访问页面时的线程安全。这一点我目前不是很理解。

  下面一段定义生成器函数,用于将获取视频帧的循环输出为byte编码的迭代器对象,进而输入给HTML的Respose:

def generate():
        # grab global references to the output frame and lock variables
        global outputFrame, lock
        while True:
            # check if the output frame is avaliable, otherwise skip
            if outputFrame is None:
                continue

            # encode the frame in JPEG format (compress raw image)
            (flag, encodedImage) = cv2.imencode(".jpg", outputFrame)
            if not flag:
                continue

            # yield the output frame in the byte format
            yield(b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' +
                bytearray(encodedImage) + b'\r\n')

  值得注意的是这里用cv2.imencode将输入的图像矩阵通过jpeg压缩了一下,然后再放到HTML上。这个细节可能在实际使用中很有帮助,因为原始图像矩阵逐像素存储数据,可能非常大,如果要以流的形式定向到网页上,会造成一些延迟。


Fig. 5 使用jpeg压缩的效果

  将迭代器对象送入Response:

@app.route("/video_feed")
def video_feed():
    # return the response generated along with the specific media
    # type (mime type)
    return Response(generate(),
        mimetype = "multipart/x-mixed-replace; boundary=frame")

  这个地方暂时还没搞明白是咋将url传递到index.html中的...

  最后就是参数解析部分:

if __name__ == "__main__":
    args = argparse.ArgumentParser()
    args.add_argument("-i", "--ip", type=str, required=True,
        help="ip address of the device")
    args.add_argument("-p", "--port", type=int, required=True,
        help="ephemeral port number of the server (1024 to 65535)")
    args.add_argument("-f", "--frame_count", type=int, default=32,
        help="# of frames used to constructh the background model")
    #TODO: algorithm related args

    args = vars(args.parse_args())

    # start a thread that will perform motion detection
    t = threading.Thread(target=detect_landmarks, args=(
        args["frame_count"],))
    t.daemon = True
    t.start()

    # start the flask app
    app.run(host=args["ip"], port=args["port"], debug=True,
        threaded=True, use_reloader=False)

vs.stop()

  最后是执行脚本的效果,见Fig 1。下面展示出审查模式下的html结构,可以看出和index非常像,唯一不同的地方是参数src传递进来了。

Fig. 6 通过inspect网页可以看出使用的模板
Fig. 7 src='/videp_feed'内容,可以看到右下角的url

相关文章

网友评论

      本文标题:Pyimagesearch CV系列之三:HTML获取OpenC

      本文链接:https://www.haomeiwen.com/subject/hmhnpctx.html