node(electron)环境中使用 onnx 模型
使用 onnx 图片识别大致流程
import ort from 'onnxruntime-node';
import MODEL_FILEPATH_PROD from '../../resources/demo.onnx?asset';
//1. 创建会话文件
let session = ort.InferenceSession.create(MODEL_FILEPATH_PROD)
//2. 明确模型inputName和outputName
console.log(session.inputName,seesion.outputName)
//3. 处理图片 使用sharp库
const { data, info } = await sharp(img)
.jpeg({ quality: 100 }) //高质量输出图片
.resize(640, 640) // 调整图像大小以匹配模型输入大小
.raw()//获取图片的原始数据
.toBuffer({ resolveWithObject: true });//输入为buffer及文件流
....一般情况还需要对数据还要进行操作 比如归一化 将图片数据转换为NCHW格式等等 具体看你模型的要求
//4. 创建张量tensor
let float32Arrary = new float32Arrary(data.length)
const tensor = new ort.Tensor('float32', float32Arrary, [1, info.channels, info.width, info.height]);
//5. 运行模型
let output = await session.run({ session.inputName: tensor });详细操作
import ort from 'onnxruntime-node';
import MODEL_FILEPATH_PROD from '../../resources/yolov8s.onnx?asset';
import { is } from '@electron-toolkit/utils';
import sharp from 'sharp';
let session;
/**
* @description 初始化onnx
*/
export async function initOnnx() {
//创建session会话文件
session = is.dev
? await ort.InferenceSession.create(MODEL_FILEPATH_PROD)
: ort.InferenceSession.create(
MODEL_FILEPATH_PROD.replace('app.asar', 'app.asar.unpacked')
); //生产环境替换文件路径因为squeezenet1_1.onnx没有被assar
}
/**
* @description 模型推理
* @param {string} img 图片路径
*/
export async function runModel(img) {
const { data, info } = await sharp(img)
.jpeg({ quality: 100 }) //高质量输出图片
.resize(640, 640) // 调整图像大小以匹配模型输入大小
.raw() //输出图片原始数据
.toBuffer({ resolveWithObject: true });
const tensor = createTensor({ data, info }); //data是图片的buffer数据,info有图片的信息 主要用到是图片的宽度(info.width)、高度(info.height)、通道数(info.channcels)
let output = await session.run({ images: tensor }); //我用的模型iutputName是images所以输入参数的key就是images。有的模型inputName可能是多个,那么参数就是{inputName[0]:tensor,inputNmae[1]:tensor1,...}
let boxes = postprocess(output.output0); //得到模型结果 我们还需要对模型数据进行处理
return boxes;
}
/**
* @description 创建张量
* @param {object} data 图片的信息
* @param {object} data.info 图片的信息
* @param {buffer} data.data 图片buffer
*/
function createTensor({ data, info }) {
const { width, height, channels } = info; //图片的宽、高、通道数
const floatArray = new Float32Array(data.length); //转成模型想要的数据类型,我这里的模型需要的是float32Arrary的数据
for (let i = 0; i < data.length; i++) {
//数据归一化,也是具体看模型需求,有的需要,有的不需要
floatArray[i] = data[i] / 255.0; //rgb的色值是0-255所以这里除以255
}
// 将图片数据转换为NCHW格式
const [r, g, b] = [
new Float32Array(width * height),
new Float32Array(width * height),
new Float32Array(width * height),
];
for (let i = 0; i < width * height; i++) {
r[i] = floatArray[i * 3];
g[i] = floatArray[i * 3 + 1];
b[i] = floatArray[i * 3 + 2];
}
const nchwData = new Float32Array(3 * width * height);
nchwData.set(r, 0);
nchwData.set(g, width * height);
nchwData.set(b, 2 * width * height);
const tensor = new ort.Tensor('float32', nchwData, [
1,
channels,
width,
height,
]); //1表示模型一次性处理图片的数量
return tensor;
}
/**
* @description 获取模型的预测结果
* @param {object} output 模型的输出
*/
function postprocess(output) {
const [batchSize, numBoxes, numAttributes] = output.dims; //dims表示结果数据的形状
const data = output.data;
let arr = reshapeArray(data, numBoxes, numAttributes);
let result = transposeArray(arr);
const boxes = isOverThreshold(result, 0.5);
return boxes;
}
/**
* @description 将一维数组转换为二维数组将(1, 84, 8400)的形状转换为(84, 8400)的形状
* @param {array} arr 一维数组
* @param {number} rows 二维数组的行数
* @param {number} cols 二维数组的列数
* @returns {array} 二维数组
*/
function reshapeArray(arr, rows, cols) {
if (arr.length !== rows * cols) {
throw new Error(
'The total size of the new array must be the same as the original array.'
);
}
let reshapedArray = [];
for (let i = 0; i < rows; i++) {
let row = arr.slice(i * cols, i * cols + cols);
reshapedArray.push(row);
}
return reshapedArray;
}
/**
* @description 将二维数组转置为(8400, 84)的形状
* @param {array} array 二维数组
* @returns {array} 二维数组
*/
function transposeArray(array) {
// 创建一个新数组
let newArray = [];
// 遍历原数组的列
for (let i = 0; i < array[0].length; i++) {
newArray[i] = [];
// 遍历原数组的行
for (let j = 0; j < array.length; j++) {
// 将原数组的行列数据交换到新数组的对应位置
newArray[i][j] = array[j][i];
}
}
return newArray;
}
/**
* @description 判断二维数组的第四个元素到最后一个元素是否大于某一个值
* @param {array} arr 二维数组
* @param {number} threshold 阈值
* @returns {array} 最终结果
*/
function isOverThreshold(arr, threshold) {
//过滤第四列到最后一列中有比阈值大的元素
let filteredRows = arr.filter((row) =>
row.slice(4).some((cell) => cell > threshold)
);
let result = filteredRows.map((i) => {
let slice = i.slice(4);
// 从slice数组中找到最大值以及下标
let tag = slice.reduce(
(acc, cur, index) => {
if (cur > acc.confidence) {
return { confidence: cur, classId: index };
} else {
return acc;
}
},
{ confidence: slice[0], classId: 0 }
);
return { point: i.slice(0, 4), ...tag };
});
//去重 并且保留最大的置信度
let RemoveDuplicates = result.reduce((ac, cur, index) => {
let tag = ac.findIndex((i) => i.classId === cur.classId);
if (tag === -1) {
ac.push(cur);
} else {
if (ac[tag].confidence < cur.confidence) {
ac[tag] = cur;
}
}
return ac;
}, []);
return RemoveDuplicates;
}名词解释和操作解释
张量:可以理解是个多维数组
buffer:文件流,就是文件的原始数据
channcels 通道数:一般图片的通道是 r g b 三个通道,但是有的图片的通道数可能不同
dims:模型推理结果的数据形状;比如返回结果的 dims:[1,84,8400];84 表示模型能识别的结果数是 80,为什么是 80 个,因为我使用的模型是图片识别,会但会识别结果的在途中的中心坐标和宽高,比如车,门,狗...。8400 表示模型推理出来 8400 个推测,每一条推测的数组长度都是 84,前四个中心点坐标和宽高,后面 80 都是每一个模型能识别的结果对应的置信度,下标减 4 就是识别结果对应的 id,通过 id 可以从结果参照表获取对应名称。
置信度:比如置信度是 0.1 就代表百分之 10 的可能性是目标结果
相关依赖
模型依赖:onnxruntime-node
图片处理库:sharp(node 图片高性能图片处理库)
图片处理
因为 onnx 模型需要对图片的尺寸有要求,一般我们都需要对图片进行处理后再放在模型中识别,node 中可以用 canvas 或者 sharp 库处理图片
通常的处理步骤(使用 sharp):
1.裁剪图片,并将图片转成 buffer 原始数据

let orimg = sharp(img); //img-图片路径
//data为图片的buffer原始数据,info记录图片信息比如宽、高
const { data, info } = await orimg
.clone()
.jpeg({ quality: 100 }) //高质量输出图片
.resize(640, 640, {
fit: 'contain',
}) // 调整图像大小以匹配模型输入大小
.raw()
.toBuffer({ resolveWithObject: true });
2.创建张量(tensor)先将图片转成 float32array(具体看模型需要的,有的可能是 int 类型的),再使用 onnxruntime 提供的方法创建张量,
/**
* @description 创建张量
* @param {object} data 图片的信息
* @param {object} data.info 图片的信息
* @param {buffer} data.data 图片buffer
*/
function createTensor2({ data, info }) {
const { width, height, channels } = info;
const floatArray = new Float32Array(data.length);
for (let i = 0; i < data.length; i++) {
floatArray[i] = data[i] / 255.0;
}
// 将图片数据转换为NCHW格式
const [r, g, b] = [
new Float32Array(width * height),
new Float32Array(width * height),
new Float32Array(width * height),
];
for (let i = 0; i < width * height; i++) {
r[i] = floatArray[i * 3];
g[i] = floatArray[i * 3 + 1];
b[i] = floatArray[i * 3 + 2];
}
const nchwData = new Float32Array(3 * width * height);
nchwData.set(r, 0);
nchwData.set(g, width * height);
nchwData.set(b, 2 * width * height);
const tensor = new ort.Tensor('float32', nchwData, [
1,
channels,
width,
height,
]);
return tensor;
}3.创建完张量后放在模型中运行得到结果,我的模型得到的结果再经过一系列处理得到一下:
//point[0],point[1]代表的是识别的框再图片的中心点的位置,point[2],point[3]表示的识别框的宽高
[
{
point: [
417.3835144042969, 254.631591796875, 241.99420166015625, 179.7368621826172,
],
confidence: 0.9593521952629089,
classId: 21,
className: 'bear',
},
];4.这是裁剪后图片识别的数据主要就是 point 识别框的位置,现在需要将识别框的还原到原始图片的位置(注意这种方式是sharp.resize的 fit 是 contian 模式)
contain 模式
/**
* @description 计算裁剪后图片上的识别框在原始图片上的位置图片裁剪模式contain
* @param {object} originalInfo - 原始图片的信息
* @param {object} resizeInfo - 裁剪图片的信息
* @param {array} boxes - 识别结果
*/
function getBoxesPosition_contain(originalInfo, resizeInfo, boxes) {
const { width: originalWidth, height: originalHeight } = originalInfo.info;
const { width: resizeWidth, height: resizeHeight } = resizeInfo.info;
const originalAspectRatio = originalWidth / originalHeight;
const targetAspectRatio = resizeWidth / resizeHeight;
let offsetX = 0,
offsetY = 0;
let scale = 1;
if (originalAspectRatio >= targetAspectRatio) {
scale = resizeWidth / originalWidth;
offsetY = (resizeHeight / scale - originalHeight) / 2;
} else {
scale = resizeHeight / originalHeight;
offsetX = (resizeWidth / scale - originalWidth) / 2;
}
boxes.forEach((item) => {
item.point[0] = Math.round(item.point[0] / scale - offsetX);
item.point[1] = Math.round(item.point[1] / scale - offsetY);
item.point[2] = Math.round(item.point[2] / scale);
item.point[3] = Math.round(item.point[3] / scale);
});
return boxes;
}
conver 模式
function getBoxesPosition_cover(originalInfo, resizeInfo, boxes) {
const { width: originalWidth, height: originalHeight } = originalInfo.info;
const { width: resizeWidth, height: resizeHeight } = resizeInfo.info;
const originalAspectRatio = originalWidth / originalHeight;
const targetAspectRatio = resizeWidth / resizeHeight;
let offsetX,
offsetY = 0;
let scale;
//判断裁剪后的是放大/缩小原始图片
if (originalAspectRatio > targetAspectRatio) {
scale = resizeHeight / originalHeight;
offsetX = (originalWidth - resizeWidth / scale) / 2;
} else {
scale = resizeWidth / originalWidth;
offsetY = (originalHeight - resizeHeight / scale) / 2;
}
boxes.forEach((item) => {
item.point[0] = Math.round(item.point[0] / scale + offsetX);
item.point[1] = Math.round(item.point[1] / scale + offsetY);
item.point[2] = Math.round(item.point[2] / scale);
item.point[3] = Math.round(item.point[3] / scale);
});
return boxes;
}5.在原始图片上绘制识别框,一下是绘制的面状识别框,如果像用线状的识别框,可将下面注释的代码解开,但是需要调整数据,因为一张图片可能会出现多个识别框。(注意 sharp 中合成图片composite中识别框的位置,宽高数据都必须是整数,否则会报错)
/**
* @description - 绘制识别结果图
* @param {string} url - 图片路径
* @param {Array<number>} point - 识别选框的位置[x,y,width,height]x y表示中心点坐标 四个值都是正整数
* @param {string>} targetPath - 保存路径
* @param {string} [line] - 识别框的粗细
* @returns {Promise<string>} - 绘制识别结果图的路径
*/
export async function rectangle(img, info) {
let rectangleList = [];
for (let i = 0; i < info.length; i++) {
if (!info[i].point.length) continue;
let e = info[i];
rectangleList.push({
input: {
create: {
width: e.point[2],
height: e.point[3],
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 0.2 },
},
},
left: e.point[0] - Math.round(e.point[2] / 2),
top: e.point[1] - Math.round(e.point[3] / 2),
});
rectangleList.push({
input: {
text: {
width: 1000,
height: 50,
rgba: true,
align: 'left',
text: `<span foreground="red">${
e.className + ' ' + (e.confidence * 100).toFixed(2) + '%'
}</span>`,
},
},
left: e.point[0] - Math.round(e.point[2] / 2),
top: e.point[1] - Math.round(e.point[3] / 2) - 55,
});
}
let data = await sharp(img).composite(rectangleList).toBuffer();
// .composite([
// // {
// // //左边竖线
// // input: {
// // create: {
// // width: info.point[2],
// // height: info.point[3],
// // channels: 4,
// // background: { r: 255, g: 0, b: 0, alpha: 0.2 },
// // },
// // },
// // left: info.point[0] - Math.round(info.point[2] / 2),
// // top: info.point[1] - Math.round(info.point[3] / 2),
// // },
// // {
// // //右边竖线
// // input: {
// // create: {
// // width: line,
// // height: info.point[3],
// // channels: 4,
// // background: { r: 255, g: 0, b: 0, alpha: 1 },
// // },
// // },
// // left: info.point[0] + Math.round(info.point[2] / 2),
// // top: info.point[1] - Math.round(info.point[3] / 2),
// // },
// // {
// // //下边横线
// // input: {
// // create: {
// // width: info.point[2],
// // height: line,
// // channels: 4,
// // background: { r: 255, g: 0, b: 0, alpha: 1 },
// // },
// // },
// // left: info.point[0] - Math.round(info.point[2] / 2),
// // top: info.point[1] + Math.round(info.point[3] / 2),
// // },
// // {
// // //上边横线
// // input: {
// // create: {
// // width: info.point[2],
// // height: line,
// // channels: 4,
// // background: { r: 255, g: 0, b: 0, alpha: 1 },
// // },
// // },
// // left: info.point[0] - Math.round(info.point[2] / 2),
// // top: info.point[1] - Math.round(info.point[3] / 2),
// // },
// // {
// // //文字
// // input: {
// // text: {
// // width: 1000,
// // height: 25,
// // rgba: true,
// // align: 'left',
// // text: `<span foreground="red">${info.className + ' ' + (info.confidence * 100).toFixed(2) + '%'}</span>`,
// // },
// // },
// // left: info.point[0] - Math.round(info.point[2] / 2),
// // top: info.point[1] - Math.round(info.point[3] / 2) - 30,
// // },
// ])
const base64 = data.toString('base64');
const dataUrl = `data:image/jpeg;base64,${base64}`;
// .toFile(targetPath); //'./combined.jpg'
return dataUrl;
}模型的流程图
在这个 ONNX 模型的流程图中,每个标签代表不同的神经网络层或操作符。常见的标签解释如下:
Conv (卷积层):Conv 表示卷积操作,它通常用于提取图像的局部特征。卷积层通过卷积核对输入数据进行滑动操作,从而提取特征映射(feature map)。
Sigmoid (Sigmoid激活函数):Sigmoid 是一种激活函数,通常用于将输出值压缩到 0 和 1 之间。这在分类任务或输出概率值时特别常用。
Mul (乘法):Mul 代表逐元素乘法操作,将两个张量的对应元素相乘。它常用于缩放或者融合特征。
Add (加法):Add 表示逐元素加法操作,将两个张量的对应元素相加。它通常用于残差网络或者融合不同层次的特征。
Relu (修正线性单元):Relu 是一种激活函数,保留输入中的正数部分,抑制负数部分。它有助于引入非线性特征并加快网络训练。
MaxPool (最大池化层):MaxPool 表示最大池化操作,它通过取池化窗口内的最大值来减少特征图的空间维度,从而保留主要特征并减少计算量。
BatchNorm (批量归一化):BatchNorm 用于对每个批次的数据进行归一化操作,从而加速训练并提高模型的稳定性。
Concat (连接):Concat 用于将两个张量在指定维度上连接,通常用于拼接不同来源的特征。
Softmax (Softmax激活函数):Softmax 用于将网络的输出转化为概率分布,特别是在多分类问题中,它将输出归一化为总和为 1 的概率值。
Gemm (广义矩阵乘法):Gemm 是一种广义矩阵乘法操作,通常用于全连接层或者其他矩阵运算。

pipeline.json文件
{
"pipeline": {
"input": ["img"],
"output": ["post_output"],
"tasks": [
{
"type": "Task",
"module": "Transform",
"name": "Preprocess",
"input": ["img"],
"output": ["prep_output"],
"transforms": [
{
"type": "LoadImageFromFile",
"backend_args": null
},
{
"type": "LetterResize",
"scale": [640, 640],
"allow_scale_up": false,
"use_mini_pad": false
},
{
"type": "Normalize",
"to_rgb": true,
"mean": [0.0, 0.0, 0.0],
"std": [255.0, 255.0, 255.0]
},
{
"type": "Pad",
"size_divisor": 1
},
{
"type": "DefaultFormatBundle"
},
{
"type": "Collect",
"meta_keys": [
"img_norm_cfg",
"ori_shape",
"img_shape",
"pad_param",
"ori_filename",
"scale_factor",
"flip",
"pad_shape",
"img_path",
"valid_ratio",
"img_id",
"flip_direction",
"filename"
],
"keys": ["img"]
}
]
},
{
"name": "yolodetector",
"type": "Task",
"module": "Net",
"is_batched": false,
"input": ["prep_output"],
"output": ["infer_output"],
"input_map": {
"img": "input"
},
"output_map": {}
},
{
"type": "Task",
"module": "mmdet",
"name": "postprocess",
"component": "ResizeBBox",
"params": {
"max_per_img": 300,
"multi_label": true,
"nms": {
"iou_threshold": 0.7,
"type": "nms"
},
"nms_pre": 30000,
"score_thr": 0.001
},
"output": ["post_output"],
"input": ["prep_output", "infer_output"]
}
]
}
}这个 pipeline.json 文件定义了一个图像处理流水线,通常用于物体检测任务。它描述了从图像输入到最终输出的步骤和每个步骤的具体操作。下面是详细的解释:
1. Pipeline 概述
- input: 输入是一个名为
img的变量,代表输入的图像。 - output: 输出是名为
post_output的变量,代表处理后的结果。 - tasks:
tasks列表定义了流水线中的不同任务,每个任务负责完成一个处理步骤。
2. Task 1: Preprocess
- type:
Task,表示这是一个任务。 - module:
Transform,表示这是一个预处理模块,执行数据转换任务。 - name:
Preprocess,任务的名称。 - input: 输入是
img,表示从输入图像开始。 - output: 输出是
prep_output,表示预处理后的输出。
预处理步骤:
- LoadImageFromFile:
- 作用是从文件中加载图像。
- LetterResize:
- 将图像缩放到 640x640,保持原始图像的纵横比。
- allow_scale_up: 如果
false,则不会将小于目标尺寸的图像放大。 - use_mini_pad: 如果
false,图像将被调整大小而不使用小填充。
- Normalize:
- 归一化图像像素值,使像素值在 0 到 1 之间。
- to_rgb: 如果为
true,表示将图像从 BGR 转换为 RGB。 - mean 和 std: 使用的均值和标准差分别是 [0.0, 0.0, 0.0] 和 [255.0, 255.0, 255.0],这意味着图像在 0 到 255 范围内直接被除以 255 进行归一化。
- Pad:
- 使用
size_divisor参数对图像进行填充。size_divisor: 1表示按 1 的倍数进行填充。
- 使用
- DefaultFormatBundle:
- 负责将图像转换为张量格式,以适配后续神经网络的输入要求。
- Collect:
- 收集一些元数据(如图像归一化配置、原始形状、缩放比例等),并指定
img作为模型输入。
- 收集一些元数据(如图像归一化配置、原始形状、缩放比例等),并指定
3. Task 2: yolodetector
- name:
yolodetector,表示这是 YOLO 模型用于物体检测的任务。 - type:
Task,属于检测网络的任务。 - module:
Net,指定为神经网络模块。 - is_batched:
false,表示单张图像推理,而不是批处理。 - input: 任务输入是
prep_output,即预处理的输出图像。 - output: 输出是
infer_output,表示 YOLO 模型的推理结果。 - input_map: 这里将
img作为 YOLO 模型的输入。 - output_map: 这里没有指定输出映射,因为输出是直接返回推理结果。
4. Task 3: postprocess
type:
Task,是后处理任务。module:
mmdet,表示该任务使用的是 MMDetection 框架。name:
postprocess,任务名称是后处理。component:
ResizeBBox,表示对检测出的边界框进行缩放。params:
max_per_img: 300,表示每张图像最多保留 300 个检测框。
multi_label:
true,表示每个检测框可以属于多个类别。nms
: 定义了非极大值抑制(NMS)的参数:
- iou_threshold: 0.7,表示在 NMS 中,如果两个边界框的 IOU 大于 0.7,则去除分数较低的框。
- type:
nms,表示使用标准的 NMS 算法。
nms_pre: 30000,表示在进行 NMS 之前,最多考虑 30000 个候选框。
score_thr: 0.001,表示只保留得分大于 0.001 的检测框。
output: 输出是
post_output,即后处理的输出结果。input: 后处理任务的输入是
prep_output和infer_output,即预处理的图像和推理的输出结果。
总结:
这段 pipeline 描述了一个从图像输入到物体检测和后处理的完整流程。具体步骤包括:
- 预处理:对图像进行缩放、归一化、填充等操作,确保其能够被神经网络正确处理。
- 推理:通过 YOLO 神经网络执行物体检测。
- 后处理:对检测出的边界框进行缩放、过滤(基于置信度阈值)和非极大值抑制,以得到最终的检测结果