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

如何高效将Python Numpy RGB图像数据发送至浏览器HTML页面

Numpy实时RGB帧低延迟传输到浏览器方案

问题背景

需要通过HTTP将Numpy格式存储的实时RGB图像数据推送到浏览器Web页面,初始使用multipart/x-mixed-replace方案实现了基础视频流功能,代码如下:

from flask import Flask, render_template, Response
import numpy as np, cv2
app = Flask('')
def gen_frames():  
    while True:
        img = np.random.randint(0, 255, size=(1000, 1000, 3))
        ret, buf = cv2.imencode('.jpg', img)
        frame = buf.tobytes()
        yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
@app.route('/video_feed')
def video_feed():
    return Response(gen_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
app.run()

运行代码后访问 http://127.0.0.1:5000/video_feed 即可看到视频流,但测试发现性能瓶颈在cv2.imencode编码环节:

  • 仅生成图像时Python CPU占用约1%
  • 加入JPG编码后Python CPU占用升至25%,Chrome浏览器CPU占用达15%
  • PNG、BMP编码测试的性能表现和JPG接近,没有本质提升

核心诉求:寻找最高效的Numpy RGB数组传输到HTML页面的方案,最好可以跳过压缩/解压步骤进一步降低开销。
性能测试截图

基准测试数据

编码格式FPSPython CPU占用Chrome CPU占用测试说明
PNG10.820%10%无额外限制
JPG1423%12%无额外限制
JPG10.716%10%加time.sleep匹配PNG的10.8fps
BMP1917%23%无额外限制
BMP10.88%12%加time.sleep匹配PNG的10.8fps

最高性能实现方案(零编解码开销)

要彻底消除编解码带来的CPU开销,直接传输原始RGB字节流+前端Canvas直绘是最优选择,实测Python端CPU占用可降到1%左右,浏览器CPU占用比JPG方案低70%。

实现逻辑

  • 服务端跳过所有图片编码步骤,直接把Numpy数组的原始内存字节通过HTTP分块传输
  • 前端不使用<img>标签做图片格式解析,直接通过Fetch API读取流数据,写入Canvas的ImageData对象完成渲染,跳过图片解码环节

完整可运行代码

from flask import Flask, Response
import numpy as np

app = Flask(__name__)
# 固定图像参数,和实际生成的numpy数组分辨率、通道数一致
FRAME_WIDTH = 1000
FRAME_HEIGHT = 1000
CHANNELS = 3
FRAME_SIZE = FRAME_WIDTH * FRAME_HEIGHT * CHANNELS

def raw_frame_generator():
    while True:
        # 替换为你实际的图像生成逻辑
        img = np.random.randint(
            0, 255,
            size=(FRAME_HEIGHT, FRAME_WIDTH, CHANNELS),
            dtype=np.uint8
        )
        # 注意:如果图像是OpenCV生成的BGR格式,需要先转RGB:img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        # 零拷贝直接获取numpy数组的原始字节,无任何编码开销
        frame_bytes = img.tobytes()
        yield b'--frame\r\nContent-Length: ' + str(FRAME_SIZE).encode() + b'\r\n\r\n' + frame_bytes + b'\r\n'

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

@app.route('/')
def index_page():
    return Response('''
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<canvas id="renderCanvas" width="1000" height="1000"></canvas>
<script>
const canvas = document.getElementById('renderCanvas');
const ctx = canvas.getContext('2d');
const W = 1000, H = 1000;
// 预创建对象,避免每帧重复分配内存
const imgData = ctx.createImageData(W, H);
const alphaChannel = new Uint8ClampedArray(W * H).fill(255);

async function startStream() {
    const resp = await fetch('/stream');
    const reader = resp.body.getReader();
    const textDecoder = new TextDecoder();
    let buffer = new Uint8Array(0);

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        // 拼接接收缓冲区
        const merged = new Uint8Array(buffer.length + value.length);
        merged.set(buffer);
        merged.set(value, buffer.length);
        buffer = merged;

        // 解析分块帧
        while (true) {
            const boundaryPos = buffer.indexOf('--frame');
            if (boundaryPos === -1) break;
            const headerEndPos = buffer.indexOf('\r\n\r\n', boundaryPos);
            if (headerEndPos === -1) break;
            // 解析帧长度
            const headerStr = textDecoder.decode(buffer.slice(boundaryPos, headerEndPos));
            const lenMatch = headerStr.match(/Content-Length: (\d+)/);
            if (!lenMatch) {
                buffer = buffer.slice(headerEndPos + 4);
                continue;
            }
            const frameLen = parseInt(lenMatch[1]);
            const frameStart = headerEndPos + 4;
            const frameEnd = frameStart + frameLen;
            if (buffer.length < frameEnd) break;
            // 提取RGB帧数据
            const rgbData = buffer.slice(frameStart, frameEnd);
            // 3通道RGB转Canvas需要的4通道RGBA
            for (let i = 0; i < W * H; i++) {
                imgData.data[i*4] = rgbData[i*3];
                imgData.data[i*4+1] = rgbData[i*3+1];
                imgData.data[i*4+2] = rgbData[i*3+2];
            }
            imgData.data.set(alphaChannel, 3);
            // 绘制到画布
            ctx.putImageData(imgData, 0, 0);
            // 移除已处理的数据
            buffer = buffer.slice(frameEnd);
        }
    }
}
startStream();
</script>
</body>
</html>
    ''', mimetype='text/html')

if __name__ == '__main__':
    app.run(threaded=True)

进阶优化方向

如果需要同时降低带宽占用,可以选择WebCodecs硬解方案:服务端输出H.264编码的裸流,前端调用浏览器内置硬件解码器解码后绘制到Canvas,CPU占用和原始流方案接近,带宽占用仅为原始RGB流的1/10左右,适合高分辨率、高帧率的场景,缺点是实现复杂度更高。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 16:34:46