Flask视频流(Video Streaming with Flask)---支持多个客户端可以同时查看视频流

使用 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>
           

猜你喜欢

转载自blog.csdn.net/weixin_46445090/article/details/144606327
今日推荐