实战精选|如何使用 OpenVINO™ 在 ElectronJS 中创建桌面应用程序

时间:2024-11-20 10:11:55

点击蓝字

关注我们,让开发变得更有趣

作者 | Mikołaj Roszczyk  华沙理工大学物联网工程师

翻译 | 武卓 英特尔 AI 软件布道师

排版 | 吴紫琴

OpenVINO™

最近,我完成了一个 demo 演示,展示了 OpenVINO™ 在 Node.js 框架中的强大功能。得益于与 Electron.js 的集成,该演示不仅能够高效地执行神经网络推理,还提供了交互式的用户体验。

OpenVINO™

应用程序概览:一种简单的背景虚化方法

这个演示展示了如何在 Node.js 环境中使用 OpenVINO™ 工具包实现背景虚化,并通过 Electron.js 创建的直观桌面界面进行呈现。启动应用程序后,您会看到一个带有黑色占位符的界面。要激活摄像头,只需从检测到的设备列表中选择您喜欢的视频源并点击“开始”。

99573ae588f3fbaa3ec23c8e1f5192ad.png

完成此步骤后,您应该会在界面中的框内看到来自摄像头的实时视频流。

2440b5b608eb8eac52c6ee27dc031ef3.png

最初,您的画面将显示为未经过任何虚化处理的原始图像。要启用背景虚化功能,请通过指定的切换开关打开推理模式。该应用还允许您选择推理设备,可以在操作前或操作过程中进行设置。当您在会话中首次选择设备时,可能会出现短暂的延迟,因为模型需要进行编译。

bcdc0c1a19a82874225c914e2360a54b.png

在摄像头画面上方,您会看到一个推理时间显示器,显示最近50帧的平均推理时间。同时,还会显示每秒处理(或推理)的帧数,让您了解性能表现。

在应用运行期间,您可以随时停止摄像头或推理,切换视频源,或更改推理设备。

OpenVINO™

项目架构

完整代码可以在 OpenVINO™ Build & Deploy 仓库中找到。该 Electron 应用项目分为几个关键部分:

GitHub - openvinotoolkit/openvino_build_deploy: Pre-built components and code samples to help you build and deploy production-grade AI applications with the OpenVINO™ Toolkit from Intel

  • 界面(Interface):

包括定义界面视觉效果的 HTML 和 CSS 文件,以及控制功能的 renderer.js 文件。

  • OpenVINO™ 任务(OpenVINO Jobs):

后端组件,位于 ov-jobs.js 文件中,包含推理、前处理、后处理和背景虚化的相关函数。

  • 主程序(Main):

main.js 文件负责启动应用程序,设置必要参数,并处理中断等操作。

  • 预加载(Preload):

preload.js 文件作为主程序与界面组件之间的桥梁,启用后端功能并处理用户触发的中断。

  • Node 模块(Node Modules):

程序所需的外部库和模块列在 package.json 文件中,同时包含其他必要的开发信息。要安装这些模块,只需在主目录运行以下命令:

npm install

保持清晰的项目架构至关重要,因为 OpenVINO™ 的 Node.js 库只能在后端运行。如果将其导入到界面层会导致错误。因此,最佳实践是将 OpenVINO™ 的导入限制在 ov-jobs.js 文件中。

OpenVINO™

它是如何工作的?

背景虚化功能使用了一个来自 TensorFlow 的分割模型,名为 “TensorFlow_Lite_Frontend_IR”,该模型已被转换为 OpenVINO™ 的 IR 格式。此模型要求输入尺寸为 1x3x256x256,因此在预处理步骤中,需要将图像调整为正方形(无需填充)。如果图像不是 RGB 格式,还需要将其颜色表示转换为 RGB 格式。

const sharp = require('sharp');


// originalImg is a sharp object.


async function preprocess(originalImg) {
    const inputSize = { w: 256, h: 256 };
    const inputImg = await originalImg
        .resize(inputSize.w, inputSize.h, { fit: 'fill' })
        .removeAlpha() // converting RGBA to RGB
        .raw()
        .toBuffer();
    const resizedImageData = new Uint8ClampedArray(inputImg.buffer);
    const tensorData = Float32Array.from(resizedImageData, x => x / 255);
    const shape = [1, inputSize.w, inputSize.h, 3];
    return new ov.Tensor(ov.element.f32, shape, tensorData);

该模型作为分类器工作,将图像中的每个像素分配到六个类别之一,其中类别 0 代表背景。在推理后,输出会经过后处理生成一个掩码,其中 0 表示背景(需要进行虚化),1 表示其他元素(需要保持清晰)。随后,该掩码会被调整为与原始图像尺寸匹配。

程序运行时采用两个主要线程:一个线程从摄像头捕获帧,根据掩码对背景进行虚化处理,并将处理后的结果显示在屏幕上。另一个线程“借用”一帧图像,运行推理以更新掩码,确保摄像头的画面保持流畅,即使某些设备上的推理需要较长时间。

OpenVINO™

在 JavaScript 中使用 OpenVINO™ 的基本功能

发现可用来进行推理的设备:

const { addon: ov } = require('openvino-node');


const core = new ov.Core();
const devices = core.getAvailableDevices();

读入和编译模型:

const { addon: ov } = require('openvino-node');


const core = new ov.Core();
const modelPath = "path/to/your/model";
const model = await core.readModel(modelPath);
const device = "AUTO"; // or "GPU", "CPU" and others
const compiledModel = await core.compileModel(model, device);

运行编译后的模型:

// have compiledModel and inputTensor prepared
let inferRequest = compiledModel.createInferRequest();
inferRequest.setInputTensor(inputTensor);
inferRequest.infer()  // inference
const outputLayer = compiledModel.outputs[0]; // only one output in this scenario
const resultTensor = inferRequest.getTensor(outputLayer); // your inference result

虚化步骤:

这是最简单的虚化图像的方式:

  1. 捕获一帧图像
    从摄像头捕获当前帧。

  2. 复制并虚化图像
    创建原始图像的副本,使用 sharp.blur() 对整个副本进行虚化处理(替代方案:OpenCV 或卷积操作)。

  3. 获取分割模型生成的掩码
    在独立线程中运行分割模型,获取当前帧的掩码。

  4. 应用掩码到虚化图像
    将虚化图像的所有像素值与掩码相乘,保留需要虚化的区域。

  5. 生成“反掩码”
    计算掩码的反值(即 1 - mask),生成“非虚化区域掩码”。

  6. 应用“反掩码”到原始图像
    将原始图像的像素值与“反掩码”相乘,保留需要保持清晰的区域。

  7. 融合两张图像
    将虚化后的图像和清晰区域的图像进行融合,生成最终图像。

在最终的应用程序代码中,多个操作被整合在一起,充分利用了 Sharp 库的功能。以下是使用 Sharp 实现的方法:

  • 根据掩码裁剪原始图像:

const sharp = require('sharp');


const person = await sharp(outputMask, {
        raw: {
            channels: 3,
            width: inputSize.w,
            height: inputSize.h,
        }
    })
        .resize(width, height, { fit: 'fill' })
        .unflatten()
        .composite([{
            input: image.data,
            raw: {
                channels: 4,
                width,
                height,
            },
            blend: 'in',
        }])
        .toBuffer();
  • 对副本进行虚化处理,并将其与裁剪后的原始图像合并(复合):

const blurSize = Math.floor(widthOfImage * 0.01)
const blurredImage = await sharp(imageToBlur.data, {
    raw: { channels: 4, width, height }, // back to RGBA
})
    .blur(blurSize)
    .composite([{
        input: person,
        raw: { channels: 4, width, height },
        blend: 'atop'
    }])
    .raw()
    .toBuffer();

OpenVINO™

虚化面临的挑战

图像虚化是一项耗时的操作,对性能影响较大,尤其是当它是图像显示线程的一部分时,这直接关系到用户体验。没有人希望视频流出现卡顿。


一种优化方法是在一开始就将图像调整为最终显示的大小。较小的图像处理速度更快,从而提高性能。

起初,我们尝试使用 OpenCV 的虚化功能(通过 opencv-wasm 模块)。然而事实证明,其速度太慢,甚至比模型推理还慢,这促使我们寻找替代方案。

我们曾设想创建一个专门用于虚化的轻量化模型。该模型以图像帧和掩码作为输入,执行“虚化过程”部分描述的数学操作,并通过深度卷积(Depthwise Convolution)实现虚化。我们使用 TensorFlow 构建了这个模型,并将其转换为 OpenVINO 格式。

  • 重要说明:OpenVINO™ 从 2024.4 版本开始支持深度卷积操作。

尽管这种方法具有潜力,但由于深度卷积速度较慢且优化不足,最终被我们放弃。

最终,我们发现 Sharp 库在此操作中表现最佳,因此将其作为项目中的解决方案。

OpenVINO™

总结

如果遵循以下关键指南,使用 Electron.js 和 OpenVINO™ 创建桌面应用程序是非常简单的:

  • 前后端功能分离:
    确保将后端功能与前端功能分开,并记住 OpenVINO™ 只能在后端使用。

  • JavaScript API 的局限性:
    并非所有 Python 中可用的 OpenVINO™ 方法都已在 JavaScript 中实现。然而,当前的 API 已足以完成大多数任务。

  • 阅读文档:
    许多常见问题的答案都可以在文档中找到。参考 OpenVINO™ Node.js API 文档以获取更多信息。

  • 检查库兼容性:
    某些 JavaScript 库可能无法很好地与 Electron 配合使用。在这些情况下,您可能需要寻找替代版本或定制分支。


别忘了亲自体验一下 "Hide Your Mess Behind" 应用程序!您可以从代码仓库运行该应用程序,也可以下载适用于 Windows(exe)或 Linux(deb、rpm)的可执行版本。

下载exe文件,一键运行安装的效果如下

OpenVINO™

---------------------------------------

*OpenVINO and the OpenVINO logo are trademarks of Intel Corporation or its subsidiaries.

-----------------------------

 OpenVINO 中文社区 

微信号 : openvinodev

B站:OpenVINO中文社区

“开放、开源、共创”

致力于通过定期举办线上与线下的沙龙、动手实践及开发者交流大会等活动,促进人工智能开发者之间的交流学习。

○ 点击 “ 在看 ”,让更多人看见