带烧瓶的视频流,多处理,CPU使用率限制在单核心级别

时间:2017-08-05 18:10:01

标签: opencv flask video-streaming flask-socketio

我正按照https://blog.miguelgrinberg.com/post/video-streaming-with-flask的说明尝试使用flask进行视频流传输。它工作正常,但我注意到了一些事情,以及一些与多处理有关的奇怪行为。

以下是代码。后台进程从摄像头源获取并将帧保存在共享阵列frame中。前台循环将帧数组编码为.jpg个字节。 Web服务器在后台守护程序线程中运行,路由/video_feed发送长连接的多部分响应。

from multiprocessing import Process, Lock, Value, Array
from threading import Thread
import sys
import ctypes
import numpy as np
import cv2
from flask import Flask, render_template, Response, request
from flask_socketio import SocketIO, send, emit

SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720

frame = np.ctypeslib.as_array(Array(ctypes.c_uint8, SCREEN_HEIGHT * SCREEN_WIDTH * 3).get_obj()).reshape(SCREEN_HEIGHT, SCREEN_WIDTH, 3)
stopped = Value(ctypes.c_bool, False)

def get_from_stream():
    stream = cv2.VideoCapture(0)
    stream.set(cv2.CAP_PROP_FPS, 30)

    while True:
        if stopped.value:
            stream.release()
            return

        _, frame_raw = stream.read()
        frame[:] = frame_raw

Process(target=get_from_stream).start()

# web server
app = Flask(__name__)
# socketio = SocketIO(app, async_mode=None)

@app.route('/')
def index():
    return render_template('index.html')

def gen():
    while True:
        yield (b'--frame\r\n'
               # b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n\r\n')
               b'Content-Type: image/jpeg\r\n\r\n' + cv2.imencode('.jpg', frame_marked)[1].tobytes() + b'\r\n\r\n')

@app.route('/video_feed')
def video_feed():
    return Response(gen(),
                    mimetype='multipart/x-mixed-replace; boundary=frame')

# @socketio.on('quit')
# def quit():
#     stopped.value = True

thread_flask = Thread(target=app.run, kwargs=dict(debug=False, threaded=True))  # threaded Werkzeug server
# thread_flask = Thread(target=socketio.run, args=(app,), kwargs=dict(debug=False, log_output=True))  # eventlet server
thread_flask.daemon = True
thread_flask.start()

while True:
    if stopped.value:
        sys.exit(0)
    frame_bytes = cv2.imencode('.jpg', frame)[1].tobytes()
    frame_marked = frame

注释掉的代码部分显示了我的实验。我注意到如果生成器gen()获得已经编码的(通过主循环)frame_bytes

b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n\r\n')

而不是在生成器内部编码,

b'Content-Type: image/jpeg\r\n\r\n' + cv2.imencode('.jpg', frame_marked)[1].tobytes() + b'\r\n\r\n')

双核Macbook Pro上的总CPU使用率始终低于100%,如果正在访问/video_feed 。如果没有人访问/video_feed,或者某人正在访问,但生成器本身对帧进行编码,则主进程占用90 +%CPU,后台get_from_stream占用20%,总计大于一个核心使用量水平。

我可以肯定这不是由于CPU使用率估计不精确(htop),并且如果有一些其他后台进程占用大量CPU,那么这些进程也会发生相同的上限,所以如果访问/video_feed并且生成器获取已编码的frame_bytes,则CPU使用率仍然低于100%。

在安装了eventlet并自动打开的flask-socketio服务器上也是如此。此外,当我注释掉这些行并运行eventlet服务器时,在生成器中编码帧时,只能处理一个订阅源请求,所有其他页面访问和socketio消息基本上都被阻止。当我重新启动程序时,那些被阻止的socketio消息被处理,因为我没有刷新页面,他们必须在前端的某个地方排队。这对于eventlet服务器来说是独一无二的,因为当我使用上面的线程Werkzeug服务器时,多个页面/提要请求可以同时发生,尽管每个提要自己编码帧,导致非常大的CPU消耗(总计接近双 - 核心水平)。 ,使用eventlet服务器,当上述CPU上限发生时(生成器获取frame_bytes),多个请求(同时多个页面/请求请求)和socketio消息是异步处理没有问题。

关于烧瓶的处理只是在我的试错中发生的相关事情。主要的问题是普通的Werkzeug服务器。我不想让每个/video_feed请求自己编码帧,是吗?让视图函数直接获取编码的字节似乎微不足道,但奇怪的是总CPU上限正在影响其他进程。

1 个答案:

答案 0 :(得分:0)

这是一个非常有趣的问题。

在方法#1中,主线程将原始视频帧编码为jpeg,视频流线程将这些视频帧传送给客户端。

在方法#2中,主线程只存储原始视频帧,视频流线程对帧进行编码并传送给客户端。

当您有一个客户端观看流时,您会认为#1应该等于或快于#2,但您的测试(以及我的测试)表明方法#2在CPU使用方面更有效

您没有考虑的是线程编码帧的速率。在方法#1中,主线程尽可能快地编码jpeg帧,而不管相机的帧速率如何,并且帧速率编码帧可以被推送到客户端。视频输入线程除了将帧推送到客户端之外没有任何其他操作,因此它可以比主线程快得多地运行并推送重复帧,这只会增加开销而没有任何好处。在方法#2中,jpeg帧的编码因网络写入而减慢,因此每单位时间编码的帧总数减少,花费的CPU就会减少。

我认为你需要为#1做出的改变尽可能高效:

  • 确保只以不高于相机帧速率的速率编码jpeg帧。
  • 确保视频Feed仅以不超过主线程编码帧速率的速率将新帧推送到客户端。

基本上,您需要添加信令机制。当捕获新帧时,opencv捕获线程应该向主线程发出信号。然后主线程应对帧进行编码,并向视频馈送线程发送信号以将其传递给客户端。例如,您可以将Event个对象用于这些信号。