一、概述
通常我们可以使用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
注意倒数第三行给了一个参数入口(用{{ }}包住的就是参数),用来传递处理之后的视频流。
下面一个函数定义我们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