You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Dash Python中如何在@app.callback回调函数内获取视频流单帧

故障根因

OpenCV 的 cv2.VideoCapture 硬件操作实例不支持多线程并发访问,你当前的代码中,视频流生成器和dcc.Interval触发的回调函数两个执行路径会同时调用camera.read()抢占摄像头资源,间隔小于200ms时抢资源的频率升高,长时间运行会积累句柄冲突、读帧异常,最终导致崩溃。另外Dash默认debug模式会启动重载进程,重复初始化摄像头,也会加剧不稳定问题。

优化方案

核心思路是统一摄像头读取入口,避免多路径直接访问硬件:

  • 启动独立后台线程专门负责读取摄像头帧,加线程锁保护更新全局最新帧缓存
  • 视频流输出、回调快照两个场景都直接从缓存读取帧,不再直接调用camera.read()
  • 禁用debug模式的自动重载,避免重复初始化摄像头占用硬件
修改后完整代码
import dash
from dash.dependencies import Output, Input
import dash_core_components as dcc
import dash_html_components as html
from flask import Flask, Response
import threading
import time
import cv2
import base64


colors = {
    'background': '#111111',
    'text': '#7FDBFF'
}

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

server = Flask(__name__)
app = dash.Dash(__name__, server=server, external_stylesheets=external_stylesheets)

# 全局帧缓存 + 线程锁
latest_frame = None
frame_lock = threading.Lock()

# 初始化摄像头
camera = cv2.VideoCapture(0)

# 后台专属读帧线程,唯一直接访问摄像头的入口
def camera_read_task():
    global latest_frame
    while True:
        success, frame = camera.read()
        if success:
            with frame_lock:
                latest_frame = frame.copy()
        # 匹配摄像头默认帧率,30帧对应约33ms间隔,可根据实际调整
        time.sleep(0.03)

# 启动后台线程,daemon=True随主进程退出自动销毁
threading.Thread(target=camera_read_task, daemon=True).start()

def video_generator():
    while True:
        with frame_lock:
            if latest_frame is None:
                time.sleep(0.01)
                continue
            frame = latest_frame.copy()
        ret, jpeg = cv2.imencode('.jpg', frame)
        frame_bytes = jpeg.tobytes()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n\r\n')

def photo_generator():
    with frame_lock:
        if latest_frame is None:
            return None
        return latest_frame.copy()


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


app.layout = html.Div([
    html.Div([
        html.H4("Webcam"),
        html.Img(src="/video_feed")
    ], style={'width': '49%', 'display': 'inline-block', 'padding-left': '25px', 'vertical-align': 'top'}),
    html.Div([
        html.H4("Snapshot"),
        dcc.Interval( 
            id = 'graph-update', 
            interval = 100, # 可下调到10ms都不会崩溃
            n_intervals = 0
        ),
        html.Img(id="image", src=app.get_asset_url('test_0.png'))
    ], style={'display': 'inline-block', 'width': '49%', 'padding-right': '0px', 'vertical-align': 'top'})
], style={'backgroundColor': colors['background']})


@app.callback(
               Output('image', 'src'),
               Input('graph-update', 'n_intervals')
             )
def update_snapshot(n):
    frame = photo_generator()
    if frame is None:
        # 帧未初始化时返回原有内容不报错
        return dash.no_update
    _, buffer = cv2.imencode('.png', frame)
    source_image = base64.b64encode(buffer).decode('utf-8')
    return 'data:image/png;base64,{}'.format(source_image)


if __name__ == '__main__':
    # 加use_reloader=False避免debug模式重复启动进程占用摄像头
    app.run_server(debug=True, use_reloader=False)
额外优化建议
  • 可在缓存中同时存储编码好的JPG/PNG字节数据,避免视频流和快照场景重复编码,进一步降低CPU占用
  • 若需要部署到多进程生产环境,可改用Redis等共享内存组件存储帧缓存,替换全局变量实现跨进程共享
  • 可增加异常捕获逻辑,摄像头断开时自动尝试重连,进一步提升长时间运行的稳定性

内容的提问来源于stack exchange,提问作者Christian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.15 01:15:07