首页
/ OpenCV.js 视频采集与显示实战:WebRTC getUserMedia、Canvas 逐帧读取与 cv.VideoCapture 完整流程

OpenCV.js 视频采集与显示实战:WebRTC getUserMedia、Canvas 逐帧读取与 cv.VideoCapture 完整流程

2026-09-05 10:16:22作者:乔或婵

本文基于 OpenCV 仓库的官方教程 js_video_display 展开,讲解如何在浏览器中通过 WebRTC 捕获摄像头实时视频流,将画面逐帧转入 cv.Mat 并用 OpenCV.js 处理显示。读完后你将掌握:navigator.mediaDevices.getUserMedia 获取媒体流、Canvas 2D API 逐帧取像素、cv.VideoCapture.read() 的底层实现与参数约束,以及用 setTimeout 控制帧率(30fps 延迟补偿)的完整可运行方案。

目标与总体思路

教程的目标很明确:从摄像头(内置或 USB)捕获视频并显示。具体示例是把摄像头画面转成灰度视频再显示。

OpenCV.js 本身运行在浏览器里,不能直接访问硬件设备。因此它采用的方案是:

  • WebRTCnavigator.mediaDevices.getUserMedia)拿到摄像头的实时媒体流;
  • HTML <video> 元素承载该流并解码;
  • HTML <canvas> 元素作为 OpenCV.js 与浏览器之间的“帧缓冲”,逐帧把 video 绘制到 canvas,再通过 getImageData() 读出像素数据填进 cv.Mat

要捕获视频,需要在网页中加入以下 HTML 元素(教程原文列举):

  • 一个 <video> 元素:直接显示来自摄像头的画面;
  • 一个 <canvas> 元素:把 video 逐帧转移到 canvas 的 ImageData;
  • 另一个 <canvas> 元素:显示 OpenCV.js 处理后的输出。

第一步:用 WebRTC 获取媒体流

先用 navigator.mediaDevices.getUserMedia 获取媒体流,并把它挂到 <video> 元素上播放:

let video = document.getElementById("videoInput"); // video is the id of video tag
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
    .then(function(stream) {
        video.srcObject = stream;
        video.play();
    })
    .catch(function(err) {
        console.log("An error occurred! " + err);
    });

仓库中的示例工具类 Utils.startCamera 对这段逻辑做了工程化封装:除了 getUserMedia 本身,它还支持按分辨率约束请求(qvga 为 320×240、vga 为 640×480),通过 canplay 事件触发回调,并提供 stopCamera() 正确释放 stream 的 video track(video.pause()video.srcObject = nullstream.getVideoTracks()[0].stop())。停止采集时正确释放媒体流是生产代码中容易遗漏的一点,可直接参考 utils.js 中的 stopCamera 实现

注意:当你播放的是视频文件而不是摄像头时,这一步是不必要的——把 <video>src 指向视频文件即可。但要留意:HTML video 元素只支持 Ogg (Theora)、WebM (VP8/VP9) 或 MP4 (H.264) 这几种封装/编码格式,其他格式在浏览器中无法直接播放。

第二步:Canvas 逐帧读取与处理

浏览器拿到摄像头流之后,用 Canvas 2D API 的 CanvasRenderingContext2D.drawImage() 把视频绘制到 canvas 上,再用 图像显示教程 中介绍的方法读取 canvas 并显示。对于视频播放,cv.imshow() 需要每隔 delay 毫秒执行一次,教程推荐用 setTimeout() 调度。若视频是 30fps,延迟应取 1000/30 - 处理耗时

let canvasFrame = document.getElementById("canvasFrame"); // canvasFrame is the id of <canvas>
let context = canvasFrame.getContext("2d");
let src = new cv.Mat(height, width, cv.CV_8UC4);
let dst = new cv.Mat(height, width, cv.CV_8UC1);

const FPS = 30;
function processVideo() {
    let begin = Date.now();
    context.drawImage(video, 0, 0, width, height);
    src.data.set(context.getImageData(0, 0, width, height).data);
    cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);
    cv.imshow("canvasOutput", dst); // canvasOutput is the id of another <canvas>;
    // schedule next one.
    let delay = 1000/FPS - (Date.now() - begin);
    setTimeout(processVideo, delay);
}

// schedule first one.
setTimeout(processVideo, 0);

逐帧管线可以概括为:drawImage(video)getImageData()src.data.set(...) → OpenCV 处理 → cv.imshow()setTimeout 自我调度。帧率控制的关键是 delay = 1000/FPS - (Date.now() - begin):用本帧实际处理耗时长出的时间从帧间隔中扣除,避免处理耗时把有效帧率拉低。

cv.VideoCapture:对上面流程的源码级封装

OpenCV.js 用上述方法实现了 cv.VideoCapture (videoSource)——使用它时无需手动添加隐藏 canvas 元素。

参数说明(继承自原文档)

  • @param videoSource 视频的 id(字符串)或 <video> 元素本身;
  • @return cv.VideoCapture 实例。

read (image) 参数说明

  • @param image 一张与视频同尺寸、且类型为 cv.CV_8UC4 的图像(出于性能考虑,图像必须预先以 CV_8UC4 类型、视频同尺寸构造,而不是每帧新建)。

这个封装的真实实现在 modules/js/src/helpers.js

Module['VideoCapture'] = function(videoSource) {
    var video = null;
    if (typeof videoSource === 'string') {
        video = document.getElementById(videoSource);
    } else {
        video = videoSource;
    }
    if (!(video instanceof HTMLVideoElement)) {
        throw new Error('Please input the valid video element or id.');
    }
    var canvas = document.createElement('canvas');   // 自动创建隐藏 canvas
    canvas.width = video.width;
    canvas.height = video.height;
    var ctx = canvas.getContext('2d');
    this.video = video;
    this.read = function(frame) {
        if (!(frame instanceof cv.Mat)) {
            throw new Error('Please input the valid cv.Mat instance.');
        }
        if (frame.type() !== cv.CV_8UC4) {
            throw new Error('Bad type of input mat: the type should be cv.CV_8UC4.');
        }
        if (frame.cols !== video.width || frame.rows !== video.height) {
            throw new Error('Bad size of input mat: the size should be same as the video.');
        }
        ctx.drawImage(video, 0, 0, video.width, video.height);
        frame.data.set(ctx.getImageData(0, 0, video.width, video.height).data);
    };
};

从源码结构看,cv.VideoCapture 内部自动 document.createElement('canvas') 并同步其尺寸,这正是教程所说“无需手动添加隐藏 canvas”的原因。read() 中有三个硬性校验,写代码前值得记住:

  1. 传入的 frame 必须是 cv.Mat 实例;
  2. frame.type() 必须是 cv.CV_8UC4(RGBA,与 canvas ImageData 的像素布局一致);
  3. frame.cols/rows 必须分别等于 video.width/video.height,否则抛异常。

read() 的最后两行 ctx.drawImage(...) + frame.data.set(ctx.getImageData(...).data) 与手工方案中的逐帧读取完全等价——封装只是把“隐藏 canvas + drawImage + getImageData”这一步藏进了对象方法里。

简化后的播放代码

有了 cv.VideoCapture,上面手写的播放代码可以简化为:

let src = new cv.Mat(height, width, cv.CV_8UC4);
let dst = new cv.Mat(height, width, cv.CV_8UC1);
let cap = new cv.VideoCapture(videoSource);

const FPS = 30;
function processVideo() {
    let begin = Date.now();
    cap.read(src);
    cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);
    cv.imshow("canvasOutput", dst);
    // schedule next one.
    let delay = 1000/FPS - (Date.now() - begin);
    setTimeout(processVideo, delay);
}

// schedule first one.
setTimeout(processVideo, 0);

注意:停止时记得 deletesrcdst OpenCV.js 的 Mat 数据分配在 WASM 堆上,不做 delete() 会造成内存泄漏;长时间运行的页面应像仓库示例一样在停止采集时成对释放(见下文完整示例)。

完整可运行示例:Start/Stop 摄像头灰度显示

仓库提供了与该教程配套的交互示例页面 js_video_display.html。页面包含一个 Start/Stop 按钮、一个 <video id="videoInput">、一个输出 <canvas id="canvasOutput"> 和一个可编辑的代码区;点击 Start 时通过 utils.startCamera('qvga', ...) 以 320×240 分辨率请求摄像头,其内嵌的处理代码是:

let video = document.getElementById('videoInput');
let src = new cv.Mat(video.height, video.width, cv.CV_8UC4);
let dst = new cv.Mat(video.height, video.width, cv.CV_8UC1);
let cap = new cv.VideoCapture(video);

const FPS = 30;
function processVideo() {
    try {
        if (!streaming) {
            // clean and stop.
            src.delete();
            dst.delete();
            return;
        }
        let begin = Date.now();
        // start processing.
        cap.read(src);
        cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);
        cv.imshow('canvasOutput', dst);
        // schedule the next one.
        let delay = 1000/FPS - (Date.now() - begin);
        setTimeout(processVideo, delay);
    } catch (err) {
        utils.printError(err);
    }
};

// schedule the first one.
setTimeout(processVideo, 0);

页面侧的配套逻辑(同样来自 js_video_display.html):

startAndStop.addEventListener('click', () => {
    if (!streaming) {
        utils.startCamera('qvga', onVideoStarted, 'videoInput');
    } else {
        utils.stopCamera();
        onVideoStopped();
    }
});

function onVideoStarted() {
    streaming = true;
    videoInput.width = videoInput.videoWidth;   // 用实际视频尺寸更新 HTML 属性
    videoInput.height = videoInput.videoHeight;
    utils.executeCode('codeEditor');            // 启动 processVideo 循环
}

这个示例演示了三件教程正文之外但实战必须处理的事:

  • 尺寸同步startCamera 启动后,video.videoWidth/videoHeight 才是摄像头实际给出的帧尺寸,需回填到 HTML 属性上;由于 cv.Mat 的宽高等于 video.width/height,而 cap.read(src) 要求 Mat 尺寸与 video 完全一致,尺寸必须对齐;
  • 停止即清理streaming 标志翻转为 false 后,下一轮 processVideo 执行 src.delete(); dst.delete(); 并返回,不再自我调度;
  • 错误兜底try/catch 捕获 WASM 异常并通过 utils.printError 展示(该函数会借助 cv.exceptionFromPtr 还原 C++ 侧异常信息,见 utils.js)。

cv.imshow 的另一半:Mat 到 canvas 的转换

cv.imshow("canvasOutput", dst) 的输出端实现在 modules/js/src/helpers.js:它先把 dstconvertTo 归一化为 CV_8U,再按通道数转成 CV_8UC4GRAY2RGBARGB2RGBA;只接受 1、3、4 通道,否则抛 Bad number of channels 异常),然后构造 ImageData 并通过 ctx.putImageData(imgData, 0, 0) 写回 canvas。这意味着本教程灰度化之后的 CV_8UC1 结果能被 imshow 直接显示,但通道数异常的 Mat 不会静默成功,这一点在调试“画面不更新”类问题时值得对照检查。

适用前提与限制小结

  • 摄像头采集依赖 WebRTC getUserMedia,运行页面须满足浏览器安全上下文(HTTPS 或本地回环)并弹出权限授权;
  • 播放视频文件时跳过 getUserMedia,但 <video> 只支持 Ogg (Theora)、WebM (VP8/VP9)、MP4 (H.264) 格式;
  • cv.VideoCapture.read() 的入参必须是与视频同尺寸的 CV_8UC4 Mat,建议复用同一块 Mat 而不是每帧新建,以获得稳定的性能表现;
  • 帧率控制遵循 delay = 1000/FPS - 处理耗时 的补偿公式,FPS 按需调整(示例取 30);
  • 停止采集时务必 delete() 所有 cv.Mat 并释放媒体流,避免 WASM 堆内存泄漏与摄像头占用。

相关延伸阅读:图像显示教程(cv.imshow 与 canvas 读取的基础方法)、js_video_display 源码示例OpenCV.js 绑定辅助实现示例工具库 Utils

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384