opencv html5,Pyimagesearch CV系列之三:HTML获取OpenCV视频流

一、概述

通常我们可以使用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的内容:

Landmarks Detection

Pi Video Surveillance

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

下面一个函数定义我们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

你可能感兴趣的:(opencv,html5)