我正按照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上限正在影响其他进程。
答案 0 :(得分:0)
这是一个非常有趣的问题。
在方法#1中,主线程将原始视频帧编码为jpeg,视频流线程将这些视频帧传送给客户端。
在方法#2中,主线程只存储原始视频帧,视频流线程对帧进行编码并传送给客户端。
当您有一个客户端观看流时,您会认为#1应该等于或快于#2,但您的测试(以及我的测试)表明方法#2在CPU使用方面更有效
您没有考虑的是线程编码帧的速率。在方法#1中,主线程尽可能快地编码jpeg帧,而不管相机的帧速率如何,并且帧速率编码帧可以被推送到客户端。视频输入线程除了将帧推送到客户端之外没有任何其他操作,因此它可以比主线程快得多地运行并推送重复帧,这只会增加开销而没有任何好处。在方法#2中,jpeg帧的编码因网络写入而减慢,因此每单位时间编码的帧总数减少,花费的CPU就会减少。
我认为你需要为#1做出的改变尽可能高效:
基本上,您需要添加信令机制。当捕获新帧时,opencv捕获线程应该向主线程发出信号。然后主线程应对帧进行编码,并向视频馈送线程发送信号以将其传递给客户端。例如,您可以将Event个对象用于这些信号。