使用 Flask 实现视频流服务
概述
本项目旨在使用 Flask 框架实现一个视频流服务,该服务能够模拟从摄像头捕获视频流,并通过模型推理对视频帧进行处理,最终将处理后的帧以 JPEG 格式编码并传输给客户端。服务同时提供状态监控功能,允许用户实时查看系统性能指标。
import threading
import time
import cv2
import numpy as np
from collections import deque
from flask import Flask, Response
class ModelInference:
def __init__(self):
# 模拟模型加载
self.model_ready = False
time.sleep(2) # 模拟模型加载时间
self.model_ready = True
def predict(self, frame):
"""模拟模型推理过程"""
# 模拟推理延迟
time.sleep(1) # 模拟每帧100ms的推理时间
# 模拟一些视觉处理效果
# 1. 添加时间戳
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
cv2.putText(frame, timestamp, (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# 2. 模拟目标检测框
height, width = frame.shape[:2]
# 随机生成一些检测框
for _ in range(3):
x = np.random.randint(0, width - 100)
y = np.random.randint(0, height - 100)
w = np.random.randint(50, 100)
h = np.random.randint(50, 100)
confidence = np.random.uniform(0.7, 0.99)
# 画框
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 显示置信度
cv2.putText(frame, f"{confidence:.2f}", (x, y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
return frame
class CameraStream:
def __init__(self, buffer_size=30):
self.camera = cv2.VideoCapture(0)
self.frame_buffer = deque(maxlen=buffer_size)
self.buffer_lock = threading.Lock()
self.is_running = True
# 记录所有活跃的消费者
self.consumers = set()
self.consumers_lock = threading.Lock()
# 初始化推理模型
self.model = ModelInference()
# 添加性能统计
self.fps = 0
self.inference_time = 0
self.last_frame_time = time.time()
# 启动生产者线程
self.producer_thread = threading.Thread(
target=self._producer_task,
daemon=True
)
self.producer_thread.start()
def _producer_task(self):
"""生产者任务:读取和处理帧"""
frame_count = 0
fps_update_interval = 30 # 每30帧更新一次FPS
while self.is_running:
ret, frame = self.camera.read()
if ret:
# 记录推理开始时间
inference_start = time.time()
# 进行模型推理
processed_frame = self._process_frame(frame)
# 计算推理时间
self.inference_time = time.time() - inference_start
# 转换为JPEG格式
ret, buffer = cv2.imencode('.jpg', processed_frame)
if ret:
jpeg_frame = buffer.tobytes()
timestamp = time.time()
with self.buffer_lock:
self.frame_buffer.append({
'frame': jpeg_frame,
'timestamp': timestamp
})
# 更新FPS计算
frame_count += 1
if frame_count % fps_update_interval == 0:
current_time = time.time()
self.fps = fps_update_interval / (current_time - self.last_frame_time)
self.last_frame_time = current_time
time.sleep(0.001) # 小的延迟以防止CPU过载
def _process_frame(self, frame):
"""使用模型处理帧"""
if self.model.model_ready:
return self.model.predict(frame)
else:
# 如果模型未就绪,显示等待信息
cv2.putText(frame, "Model Loading...", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
return frame
def register_consumer(self):
"""注册新的消费者"""
consumer_id = id(threading.current_thread())
with self.consumers_lock:
self.consumers.add(consumer_id)
return consumer_id
def unregister_consumer(self, consumer_id):
"""注销消费者"""
with self.consumers_lock:
self.consumers.discard(consumer_id)
def get_frame_generator(self):
"""为每个消费者创建独立的帧生成器"""
consumer_id = self.register_consumer()
last_frame_time = 0
try:
while self.is_running:
frame_data = None
with self.buffer_lock:
if self.frame_buffer:
newest_frame = self.frame_buffer[-1]
# 只有当有新帧时才发送
if newest_frame['timestamp'] > last_frame_time:
frame_data = newest_frame
last_frame_time = newest_frame['timestamp']
if frame_data:
# 直接使用已经编码好的JPEG数据
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' +
frame_data['frame'] +
b'\r\n')
time.sleep(1 / 30) # 控制消费帧率
finally:
self.unregister_consumer(consumer_id)
def get_stats(self):
"""获取当前状态"""
with self.consumers_lock:
consumer_count = len(self.consumers)
with self.buffer_lock:
buffer_size = len(self.frame_buffer)
return {
'active_consumers': consumer_count,
'buffer_size': buffer_size,
'fps': round(self.fps, 1),
'inference_time': round(self.inference_time * 1000, 1), # 转换为毫秒
'model_ready': self.model.model_ready
}
def cleanup(self):
"""清理资源"""
self.is_running = False
self.producer_thread.join()
self.camera.release()
# Flask应用
app = Flask(__name__)
camera_stream = CameraStream()
@app.route('/video_feed')
def video_feed():
return Response(
camera_stream.get_frame_generator(),
mimetype='multipart/x-mixed-replace; boundary=frame'
)
@app.route('/stats')
def stats():
return camera_stream.get_stats()
@app.route('/')
def index():
"""返回HTML页面"""
return """
<html>
<head>
<title>摄像头流</title>
<style>
.stats {
margin: 20px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-family: monospace;
}
</style>
</head>
<body>
<h1>实时视频流</h1>
<img src="/video_feed">
<div id="stats" class="stats"></div>
<script>
setInterval(async () => {
const response = await fetch('/stats');
const data = await response.json();
document.getElementById('stats').innerHTML = `
<div>活跃连接数:${data.active_consumers}</div>
<div>缓冲区大小:${data.buffer_size}</div>