news 2026/9/6 22:04:48

OpenCV.js dnn 图像分类实战:基于摄像头的实时分类推理完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCV.js dnn 图像分类实战:基于摄像头的实时分类推理完整指南

OpenCV.js dnn 图像分类实战:基于摄像头的实时分类推理完整指南

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

本文基于 OpenCV 仓库中的 JavaScript 教程文档 js_image_classification_with_camera.markdown,讲解如何用 OpenCV.js 的dnn模块实现“摄像头实时画面 + 深度学习模型”的图像分类应用。通过本文,读者将掌握从摄像头采集视频帧、cv.blobFromImage前处理、cv.readNet/net.forward()模型推理,到 softmax 后处理与 Top-K 结果展示的全链路实现,并可复用到任意 ONNX/TensorFlow 分类模型。

教程目标与配套示例

原教程文档给出的目标非常明确:

学习如何使用 OpenCV.js 的 dnn 模块,完成一个带摄像头的图像分类示例。

教程配套的可交互示例页面是 js_image_classification_with_camera.html,它由以下部分组成:

  • 一个Start/Stop按钮,控制摄像头采集的开始与停止;
  • 一个<video id="videoInput">元素(400×400)用于显示摄像头原始画面;
  • 一个结果表格,展示模型输出的 Top-3 标签(Label)与概率(Probability),以及本次推理耗时;
  • 一个modelFile文件上传控件,用于把本地 ONNX 模型文件写入 Emscripten 虚拟文件系统;
  • 六个textarea.code代码编辑区(codeEditorcodeEditor5),分别展示并在线可执行六段核心代码,读者可以直接在浏览器里修改参数并实时观察效果;
  • 一个 Model Info 附录区域,由 js_dnn_example_helper.js 中的drawInfoTable()从 js_image_classification_model_info.json 动态渲染,列出各模型对应的下载链接与推理参数。

原文档还特别提示:如果不了解如何从摄像头采集视频,应先阅读 js_video_display.markdown(视频采集入门教程)。

模型推理参数:决定“能否换模型”的关键配置

示例第一段代码(codeSnippet)定义了与具体模型强相关的推理参数,这是把示例适配到新模型时唯一必须修改的配置区:

inputSize = [224, 224]; mean = [104, 117, 123]; std = 1; swapRB = false; // record if need softmax function for post-processing needSoftmax = false; // url for label file, can from local or Internet labelsUrl = "见模型信息 JSON 中的 labelsUrl 字段";

各参数含义与来源:

参数含义说明
inputSize网络输入尺寸[宽, 高]传给cv.blobFromImage,必须与模型实际输入匹配(如 224×224)
mean均值向量blobFromImage中对每个通道减去的均值,归一化方式取决于模型训练框架
std标准差在示例的getBlobFromImage中作为scale传入,即每个像素除以1/std
swapRB是否交换 R/B 通道BGR 训练框架(Caffe/CV)模型通常为true,RGB 框架(PyTorch)为false
needSoftmax输出是否需要补 softmax若模型末尾已带 softmax 层(如 TensorFlow Inception 图)则为false
labelsUrl类别标签 txt 地址每行一个标签,可来自本地或网络,loadLables()fetch并按换行切分

仓库中 js_image_classification_model_info.json 给出了 5 个 ONNX 分类模型与 1 个 TensorFlow 模型的完整参数对照表,是调参时最直接的参考。整理如下(模型下载链接以 JSON 文件中的实际 URL 为准):

框架模型inputSizemeanstdscaleswapRBneedSoftmax
onnxgooglenet224, 224103.939, 116.779, 123.6751, 1, 11falsetrue
onnxsqueezenet224, 2240.485, 0.456, 0.4060.229, 0.224, 0.2250.003921truetrue
onnxresnet224, 224123.675, 116.28, 103.5358.395, 57.12, 57.3751truetrue
onnxvgg16224, 224103.939, 116.779, 123.681, 1, 11falsetrue
onnxdensenet121224, 224123.675, 116.28, 103.530.229, 0.224, 0.2250.003921truetrue
tensorflowinception224, 224123, 117, 10411truefalse

从这张表可以直观看到:不同训练框架(Caffe 风格 mean 约 104/117/123,PyTorch 风格 mean 0.485…、scale 0.003921)的归一化参数差异很大,needSoftmax也随模型末层结构而变化——这正是“换模型时只改第一段代码”的必要性所在。

核心代码一:视频捕获主循环与每帧推理

示例第二段代码(codeSnippet1)是整个应用的骨架,包含三块:帧缓冲与cv.VideoCapture的创建、每帧推理入口main()、以及驱动主循环的processVideo()

let frame = new cv.Mat(video.height, video.width, cv.CV_8UC4); let cap = new cv.VideoCapture(video); main = async function(frame) { const labels = await loadLables(labelsUrl); const input = getBlobFromImage(inputSize, mean, std, swapRB, frame); let net = cv.readNet(modelPath); net.setInput(input); const start = performance.now(); const result = net.forward(); const time = performance.now()-start; const probs = softmax(result); const classes = getTopClasses(probs, labels); updateResult(classes, time); setTimeout(processVideo, 0); input.delete(); net.delete(); result.delete(); } function processVideo() { try { if (!streaming) { return; } cap.read(frame); main(frame); } catch (err) { utils.printError(err); } } setTimeout(processVideo, 0);

从源码结构看,这段循环有几个值得注意的设计:

  1. frame必须是CV_8UC4且尺寸与视频一致。这是 js_video_display.markdown 中明确给出的性能约束:cv.VideoCapture.read()会把<video>经 canvas 读出的 RGBA 像素直接写入该Matdata,避免中途转换。
  2. setTimeout(processVideo, 0)的协作式调度。每帧推理结束后(main是 async 函数)再排队下一帧,天然形成“上一帧完成才读下一帧”的节奏,避免帧堆积;这与视频入门教程推荐的setTimeout定时驱动方式一致(该教程建议 30fps 视频使用delay = 1000/FPS - 处理耗时,这里用 0 表示“尽快处理,由推理耗时自然限速”)。
  3. 每帧都执行cv.readNet(modelPath)net.delete()。模型已被loadModel()写入 Emscripten 文件系统(路径modelPath),这里逐帧重新加载便于演示单帧完整链路;在追求帧率的生产代码中,把readNet提到主循环外只加载一次是常见优化(源码中net.delete()的写法说明这是刻意配合逐帧加载的资源管理)。
  4. Emscripten 内存必须手动delete()inputnetresult三处显式释放,这是 OpenCV.js 中所有 JS 侧持有的Mat/Net对象的通用要求。

核心代码二:前处理、标签加载与模型写入文件系统

以下三段工具函数定义在公共助手文件 js_dnn_example_helper.js 中,被本示例和同目录的其他 dnn 示例(物体检测、姿态估计、风格迁移等)共用。

1. 标签加载loadLables

loadLables = async function(labelsUrl) { let response = await fetch(labelsUrl); let label = await response.text(); label = label.split('\n'); return label; }

fetch一个每行一个类别名的 txt 文件(分类模型常用 ILSVRC2012/ImageNet 标签表,地址见 js_image_classification_model_info.json 中的labelsUrl字段),按换行切分为数组,下标与网络输出通道一一对应。

2. 图像转 BlobgetBlobFromImage

getBlobFromImage = function(inputSize, mean, std, swapRB, image) { let mat; if (typeof(image) === 'string') { mat = cv.imread(image); } else { mat = image; } let matC3 = new cv.Mat(mat.matSize[0], mat.matSize[1], cv.CV_8UC3); cv.cvtColor(mat, matC3, cv.COLOR_RGBA2BGR); let input = cv.blobFromImage(matC3, std, new cv.Size(inputSize[0], inputSize[1]), new cv.Scalar(mean[0], mean[1], mean[2]), swapRB); matC3.delete(); return input; }

这个函数把“摄像头帧/本地图片 → 模型输入”的标准前处理压缩为三步:

  • 通道准备:输入是CV_8UC3则直接使用,是路径字符串则cv.imread,否则(如CV_8UC4视频帧)先用cv.cvtColor转成CV_8UC3的 BGR;
  • 缩放 + 归一化cv.blobFromImage(src, scale, size, mean, swapRB)一步完成缩放到inputSize、减去mean、乘以scale,并按swapRB决定是否 R/B 互换;
  • 资源回收:中间量matC3用完即delete()

注意示例中把第一段的std直接当作scale传入,因此模型信息表里的scale列(如 squeezenet 的 0.003921,即 1/255)实际承担了“像素值缩放到 0~1”的职责,std列则是blobFromImage语义下的归一化参数——理解这层映射关系是正确移植其他模型的前提。

3. 模型文件写入虚拟文件系统loadModel

loadModel = async function(e) => { return new Promise((resolve) => { let file = e.target.files[0]; let path = file.name; let reader = new FileReader(); reader.readAsArrayBuffer(file); reader.onload = function(ev) { if (reader.readyState === 2) { let buffer = reader.result; let data = new Uint8Array(buffer); cv.FS_createDataFile('/', path, data, true, false, false); resolve(path); } } }); }

浏览器无法直接让 WASM 版 OpenCV 读取本地文件,这里的方案是:FileReader把用户通过modelFile控件选中的 ONNX 文件读成ArrayBuffer,再用 Emscripten 的cv.FS_createDataFile()写入虚拟文件系统的根目录(路径即原文件名)。示例页面在change事件回调中调用它并保存返回值:

let modelPath = ""; let modelFile = document.getElementById('modelFile'); modelFile.addEventListener('change', async (e) => { initStatus(); modelPath = await loadModel(e); document.getElementById('status').innerHTML = `The model file '${modelPath}' is created successfully.`; });

此后cv.readNet(modelPath)就能按普通文件路径加载模型。这也是 Web 端加载模型最通用的模式:模型以文件形式(ONNX、TensorFlow protobuf)预先存在,运行时落入虚拟文件系统,由dnn读取。

核心代码三:后处理——softmax 与 Top-K 提取

最后一组代码(codeSnippet5及助手中的getTopClasses)负责把网络输出的原始 logits/prob 向量变成人类可读的标签与置信度。

softmax = function(result) { let arr = result.data32F; if (needSoftmax) { const maxNum = Math.max(...arr); const expSum = arr.map((num) => Math.exp(num - maxNum)).reduce((a, b) => a + b); return arr.map((value, index) => { return Math.exp(value - maxNum) / expSum; }); } else { return arr; } }

result.data32F直接从输出Mat取出 32 位浮点数组;只有当needSoftmax === true(模型末层没有 softmax)时才做数值稳定的 softmax(先减maxNum防止exp上溢)。若模型输出已是概率(如 TensorFlow Inception 图,needSoftmaxfalse),则原样返回。

getTopClasses = function(probs, labels, topK = 3) { probs = Array.from(probs); let indexes = probs.map((prob, index) => [prob, index]); let sorted = indexes.sort((a, b) => { if (a[0] === b[0]) {return 0;} return a[0] < b[0] ? -1 : 1; }); sorted.reverse(); let classes = []; for (let i = 0; i < topK; ++i) { let prob = sorted[i][0]; let index = sorted[i][1]; let c = { label: labels[index], prob: (prob * 100).toFixed(2) } classes.push(c); } return classes; }

getTopClasses把概率数组转为[prob, index]对,降序排序后取前topK(默认 3)个,返回{label, prob}对象数组;prob已被换算为百分比并保留两位小数。页面端的updateResult()再把它们填进结果表格的label0/1/2prob0/1/2单元格,并在状态栏显示模型路径与Inference time: xx.xx ms(即performance.now()前后差值)。

摄像头捕获与运行流程

示例的运行入口由页面脚本控制,关键流程是:

let video = document.getElementById('videoInput'); let streaming = false; let startAndStop = document.getElementById('startAndStop'); startAndStop.addEventListener('click', () => { if (!streaming) { utils.clearError(); utils.startCamera('qvga', onVideoStarted, 'videoInput'); } else { utils.stopCamera(); onVideoStopped(); } });
  • 点击 Start 时调用utils.startCamera('qvga', ...)(实现见 utils.js),底层即 js_video_display.markdown 讲解的navigator.mediaDevices.getUserMedia({ video: true, audio: false }):浏览器通过 WebRTC 拿到摄像头媒体流赋给<video>元素,请求分辨率档位为 QVGA(320×240)——对 224×224 的输入网络而言,小分辨率采集能显著降低每帧前处理与传输开销;
  • onVideoStarted()streaming置为true,并执行codeEditorcodeEditor1两段代码,创建cv.Mat/cv.VideoCapture并启动processVideo()循环;
  • 点击 Stop 时utils.stopCamera()释放摄像头,onVideoStopped()复位状态。

因此完整的运行时序为:上传模型 → 写入虚拟文件系统 → 点击 Start 打开摄像头 →cap.read(frame)逐帧读取 RGBA 帧 →blobFromImage前处理 →readNet+forward()推理 → softmax + Top-K → 表格刷新与耗时显示 →setTimeout(processVideo, 0)进入下一帧

需要注意的环境前提:

  • getUserMedia在浏览器中通常要求安全上下文https://localhost),本地纯file://方式打开页面时摄像头接口可能不可用;
  • OpenCV.js 需先由utils.loadOpenCv()加载完成(示例在回调后才启用 Start 按钮),加载来源配置见 js_setup.markdown 与 js_usage.markdown;
  • labelsUrl使用fetch跨域获取时,目标地址需允许匿名访问(仓库示例使用 OpenCV 官方数据目录中的 ImageNet 标签表)。

小结与可复用要点

  • 本教程文档(js_image_classification_with_camera.markdown)是 OpenCV.js dnn 系列教程中“实时视频版”的图像分类篇,其完整可运行形态由 js_image_classification_with_camera.html、js_dnn_example_helper.js 和 js_image_classification_model_info.json 三者共同构成;
  • 换一个分类模型只需改一处:第一段的inputSize / mean / std / swapRB / needSoftmax / labelsUrl,参数对照直接查模型信息 JSON;
  • Web 端加载模型的标准姿势是FileReader+cv.FS_createDataFile()写入 Emscripten 虚拟文件系统,再cv.readNet(path)
  • 视频帧必须以CV_8UC4、与视频等尺寸的Matcv.VideoCapture.read()写入;所有 JS 侧创建的Mat/Net用完都要delete()
  • 主循环用setTimeout(processVideo, 0)协作式调度,推理耗时可用performance.now()精确测量并展示,便于评估模型在浏览器端的实际吞吐。

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 22:03:55

STM32F030 HAL库 ADC多通道单次采集实战详解

简介&#xff1a;面向嵌入式开发者的STM32F030 ADC多通道采集配置方法文档&#xff0c;重点解决使用HAL库和CubeMX工具时的初始化、通道配置与数据读取问题。内容从ADC_HandleTypeDef实例化、参数设置&#xff0c;到通道排序、采样时间选择&#xff0c;再到启动转换、轮询结果并…

作者头像 李华
网站建设 2026/9/6 22:03:50

PSASP九节点系统暂态稳定分析全流程解析

简介&#xff1a;关于使用PSASP进行九节点电力系统暂态稳定分析的专业技术资料&#xff0c;面向电气工程专业学生、电网调度运行人员、继电保护工程师及电力系统仿真研究初入者&#xff0c;重点解决大扰动下系统能否保持同步运行、如何通过潮流计算与故障仿真判断暂态稳定性的实…

作者头像 李华
网站建设 2026/9/6 22:03:45

UVR 人声分离指南:三步从歌曲里拿到无原唱伴奏

UVR 人声分离指南&#xff1a;三步从歌曲里拿到无原唱伴奏 【免费下载链接】ultimatevocalremovergui GUI for a Vocal Remover that uses Deep Neural Networks. 项目地址: https://gitcode.com/GitHub_Trending/ul/ultimatevocalremovergui 想把一首歌的人声去掉、只…

作者头像 李华
网站建设 2026/9/6 21:59:18

通达信慧眼K线主图指标:双均线交叉+量能确认的买卖点公式详解

简介&#xff1a;这是一份面向通达信软件用户与股票技术分析爱好者的K线主图指标公式源码文档&#xff0c;核心解决行情主图信号可视化与买卖参考识别问题。文档内含完整的指标公式源码及变量释义&#xff0c;涵盖牛线、马线、买卖线、主力成本线、资金生命线、水平面等核心曲线…

作者头像 李华
网站建设 2026/9/6 21:57:40

光敏电阻基本特性实验指南:从原理到伏安特性与光照特性测量

简介&#xff1a;这是一份面向电子信息、光电检测等课程学习者的光敏电阻基本特性实验文档&#xff0c;适用于本科或高职院校学生在“传感器技术”“光电检测技术”等实验环节中参考使用。文档围绕光敏电阻的工作原理展开&#xff0c;系统讲解了光电导效应、暗电阻与亮电阻的测…

作者头像 李华