如何使用Yolov8神经网络和JavaScript在Web浏览器中检测视频中的对象
#javascript #网络开发人员 #ai #computervision

目录

Introduction
Adding a video component to a web page
Capture video frames for object detection
Detect objects in video
Prepare the input
Run the model
吃了abiaoaqian8
Draw bounding boxes
Running several tasks in parallel in JavaScript
Running the model in a background thread
Conclusion

介绍

这是Yolov8系列的第三部分。在上一部分中,我指导您完成所有Yolov8 Essentials,包括数据准备,神经网络培训和图像上的对象检测。最后,我们创建了一个Web服务,该服务使用不同的编程语言在图像上检测对象。

现在是时候向前迈进了一步了。如果您知道如何检测图像中的对象,那么没有什么可以阻止您检测到视频中的对象,因为视频是带有背景声音的图像。您只需要知道如何将每个帧作为图像捕获,然后使用我们在previous article中编写的相同代码将其通过对象检测神经网络将其传递。这就是我要在本教程中展示的内容。

在下一节中,我们将创建一个Web应用程序,该应用程序将在视频中检测到已加载到Web浏览器的视频中。它将实时显示检测到对象的边界框。最终应用将如下一个视频所示。

确保您阅读并尝试了本系列的所有先前文章,尤其是How to detect objects on images using JavaScript部分,因为我将重复使用该项目的算法和源代码。

刷新您有关如何使用Yolov8神经网络来检测网络浏览器中图像上的对象的知识后,您将准备继续阅读下面的部分。

将视频组件添加到网页

让我们立即开始一个项目。创建一个新文件夹,并使用以下内容添加index.html文件:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Object detector</title>    
</head>
<body>
<video src="sample.mp4" controls></video>
</body>
</html>

我们使用

video元素具有许多属性。我们使用src指定源视频文件和controls属性,以使用play和其他按钮显示控制栏。 video标签选项的完整列表,您可以找到here

打开此网页时,您将看到以下内容:

Image description

您可以看到它显示了视频和底面板,可用于控制视频:播放/暂停,更改音频卷,以全屏模式显示等等。

另外,您可以从JavaScript代码管理此组件。要从您的代码中访问视频元素,您需要获取指向视频对象的链接:

const video = document.querySelector("video");

然后,您可以使用video对象来编程控制视频。该变量是实现HTMLMediaElement接口的HTMLVideoElement对象的实例。该对象包含一组属性和方法来控制视频元素。此外,它还可以访问视频生命周期事件。您可以绑定事件处理程序以对许多不同的事件做出反应:

  • loadeddata-在视频加载并显示第一帧时被解雇
  • play-视频开始播放时被解雇
  • pause-视频暂停时捕获

您可以使用这些事件捕获视频帧。在捕获帧之前,您需要知道视频的尺寸:宽度和高度。让我们立即加载视频后立即获取。

使用object_detector.js名称创建一个JavaScript文件,并将其包含在index.html

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Object detector</title>    
</head>
<body>
<video src="sample.mp4" controls></video>
<script src="object_detector.js" defer></script>
</body>
</html>

并将以下内容添加到新文件:

object_detector.js

const video = document.querySelector("video");

video.addEventListener("loadeddata", () => {
    console.log(video.videoWidth, video.videoHeight);
})

在此代码段中,您为videoloadeddata事件设置了事件侦听器。当将视频文件加载到视频元素时,视频的尺寸就会可用,然后将videoWidthvideoHeight打印到控制台。

如果您使用了sample.mp4视频文件,则应在控制台上看到以下大小。

960 540

如果有效,则可以捕获视频帧的一切。

捕获视频帧以进行对象检测

正如您在previous article中应该阅读的那样,要检测图像上的对象,您需要将图像转换为标准化像素颜色的数组。为此,我们使用drawImage方法在HTML5 canvas上画了图像,然后我们使用getImageData HTML5 Canvas contextgetImageData方法来访问像素及其颜色组件。

>

关于drawImage方法的伟大之处是,您可以使用它以与绘制图像相同的方式在画布上绘制视频。

让我们看看它是如何工作的。将元素添加到index.html页:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Object detector</title>
</head>
<body>
<video src="sample.mp4" controls></video>
<br/>
<canvas></canvas>
<script src="object_detector.js" defer></script>
</body>
</html>

用户按下“播放”按钮时,视频组件开始播放视频,或者开发人员在video对象上调用play()方法。这就是为什么要开始捕获视频的原因,您需要实现play事件侦听器的原因。将object_detector.js文件的内容替换为以下内容:

object_detector.js

const video = document.querySelector("video");

video.addEventListener("play", () => {
    const canvas = document.querySelector("canvas");
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const context = canvas.getContext("2d");
    context.drawImage(video,0,0);
});

在此代码中,视频开始播放时:

  • 触发了“播放”事件侦听器。
  • 在此事件处理功能时,我们设置了具有实际宽度和视频高度的canvas元素
  • 下一个代码获得对2D HTML5画布绘制context的访问权限
  • 然后,使用drawImage方法,我们在画布上绘制视频。

在Web浏览器中打开index.html页面,然后按“ Play”按钮。之后,您应该看到以下内容:

Image description

在这里,您可以在顶部和画布上看到视频,并在其下方看到捕获的框架。画布仅显示第一帧,因为当视频启动时,您只捕获了一次帧。要捕获每个帧,您需要在视频播放时一直致电drawImage。您可以使用setInterval函数重复调用指定的代码。让我们每30毫秒绘制当前视频的框架:

object_detector.js

let interval;
video.addEventListener("play", () => {
    const canvas = document.querySelector("canvas");
    const context = canvas.getContext("2d");
    const interval = setInterval(() => {
        context.drawImage(video,0,0);
    },30)
});

在此代码中,我们在播放视频时绘制当前的视频框架。但是,如果视频停止播放,我们应该停止此过程,因为如果暂停或结束了视频,就没有任何时间重新绘制画布。为此,我将创建的间隔保存到了interval变量,该变量可以在clearInterval函数中使用。

要拦截视频停止播放的时刻,您需要处理pause事件。在视频停止播放时,将以下内容添加到您的代码中以停止捕获帧:

object_detector.js

video.addEventListener("pause", () => {
    clearInterval(interval);
});

完成此操作后,您可以重新加载页面。如果一切都正确完成,则按下“播放”按钮时,您会看到视频和画布同步。

Image description

setInterval功能中的代码将捕获并绘制画布上的每个帧,直到播放视频为止。如果按“暂停”按钮或视频结束,则pause事件处理程序将清除间隔并停止捕获循环。

我们不需要在网页上两次显示同一视频,因此我们将自定义我们的播放器。让我们隐藏原始的video播放器,只留下画布。

index.html

<video controls style="display:none" src="sample.mp4"></video>

但是,如果我们隐藏了视频播放器,那么我们将无法访问“ play”和“暂停”按钮。幸运的是,这不是一个大问题,因为您可以通过编程方式控制video对象。它具有控制播放的playpause方法。我们将在画布下方添加自己的“播放”和“暂停”按钮,这就是新UI的外观:

index.html

<video controls style="display:none" src="sample.mp4"></video><br/>
<canvas></canvas><br/>
<button id="play">Play</button>&nbsp;
<button id="pause">Pause</button>

现在,将创建按钮的onclick事件处理程序添加到object_detector.js

object_detector.js

const playBtn = document.getElementById("play");
const pauseBtn = document.getElementById("pause");
playBtn.addEventListener("click", () => {
    video.play();
});
pauseBtn.addEventListener("click", () => {
    video.pause();
});

更改后的页面以查看结果:

Image description

您应该能够通过按“播放”按钮开始播放,然后按“暂停”按钮。

这是当前阶段的完整JavaScript代码:

object_detector.js

const video = document.querySelector("video");
let interval
video.addEventListener("play", () => {
    const canvas = document.querySelector("canvas");
    const context = canvas.getContext("2d");
    interval = setInterval(() => {
        context.drawImage(video,0,0);

    },30)
});

video.addEventListener("pause", () => {
    clearInterval(interval);
});

const playBtn = document.getElementById("play");
const pauseBtn = document.getElementById("pause");
playBtn.addEventListener("click", () => {
    video.play();
});
pauseBtn.addEventListener("click", () => {
    video.pause();
});

现在,您拥有自定义的视频播放器,并且可以完全控制视频的每个帧。例如,您可以使用HTML5 Canvas context API在任何视频框架上绘制您想要的任何内容。在下面的部分中,我们将将每个帧传递到Yolov8神经网络,以检测其上的所有对象并在它们周围绘制边界框。我们将使用与上一篇文章中编写的相同代码,当开发JavaScript object detection web service以准备输入,运行模型,处理输出并围绕检测到的对象绘制边界框。

检测视频中的对象

要检测视频中的对象,您需要在视频的每个帧上检测对象。您已经将每个帧转换为图像并将其显示在HTML5画布上。一切都准备好重复使用代码,我们在previous article, to detect objects on image中编写了代码。对于每个视频框架,您需要:

  • 从画布上的图像中准备输入
  • 使用此输入运行模型
  • 处理输出
  • 在每个帧顶部显示有界的检测到的对象

准备输入

让我们创建一个prepare_input函数,该功能将用于准备神经网络模型的输入。此功能将接收带有显示框架的canvas作为图像,并将对其进行以下操作:

  • 创建临时画布并将其调整为640x640,这是yolov8 Model所需的
  • 将源图像(画布)复制到此临时画布
  • 使用HTML5 Canvas Context的getImageData方法获取像素颜色组件的数组
  • 将每个像素的红色,绿色和蓝色组件收集到分开阵列
  • 将这些阵列连接到红色首先进行的单个阵列,接下来是绿色的,布鲁斯持续了。
  • 返回此数组

让我们实现此功能:

object_detector.js

function prepare_input(img) {  
    const canvas = document.createElement("canvas");
    canvas.width = 640;
    canvas.height = 640;
    const context = canvas.getContext("2d");
    context.drawImage(img, 0, 0, 640, 640);

    const data = context.getImageData(0,0,640,640).data;
    const red = [], green = [], blue = [];
    for (let index=0;index<data.length;index+=4) {
        red.push(data[index]/255);
        green.push(data[index+1]/255);
        blue.push(data[index+2]/255);
    }
    return [...red, ...green, ...blue];
}

在函数的第一部分中,我们创建了一个640x640大小的隐形画布,并在其上显示了输入图像,调整大小为640x640。

然后,我们可以访问Canvas Pixels数据,收集到greenredblue阵列的颜色组件,将它们加入并返回。此过程显示在下一个图像上。

Image description

另外,我们将每个颜色成分值归一化,将其除以255。

此函数与previous article中创建的prepare_input函数非常相似。我们不需要在此处为​​图像创建HTML元素的唯一区别,因为图像已经存在于输入画布上。

当准备好此功能时,您可以将每个帧传递给它,并接收以用作Yolov8模型的输入的数组。将呼叫添加到此功能到setInterval循环:

object_detector.js

interval = setInterval(() => {
    context.drawImage(video,0,0);
    const input = prepare_input(canvas);
},30)

在这里,在画布上绘制框架后,您将图像上的画布传递给了prepare_input函数,该功能返回了该框架所有像素的红色,绿色和蓝色组件。该数组将用作Yolov8神经网络模型的输入。

运行模型

当输入准备就绪时,是时候将其传递到神经网络了。我们不会为此创建后端,一切都将在前端起作用。我们将使用ONNX运行时库的JavaScript版本在浏览器中直接运行模型预测。将ONNX运行时JavaScript库包含到index.html文件以加载它。

index.html

<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>

然后,您需要获取Yolov8型号并将其转换为ONNX格式。如上一篇文章的this section所述。将导出的.onnx文件复制到使用index.html的同一文件夹。

然后,让我们编写一个函数run_model,该函数将使用.oonx文件实例化模型,然后将传递以上部分准备的输入到模型,并将返回原始预测:

object_detector.js

async function run_model(input) {
    const model = await ort.InferenceSession.create("yolov8n.onnx");
    input = new ort.Tensor(Float32Array.from(input),[1, 3, 640, 640]);
    const outputs = await model.run({images:input});
    return outputs["output0"].data;
}

此代码仅在上一篇文章的appropriate section中复制/粘贴。阅读更多以刷新您对其工作原理的了解。

在这里,我使用了yolov8n.onnx型号,这是可可数据集上预读的Yolov8模型的微小版本。您可以在此处使用任何其他审慎或自定义模型。

最后,在您的setInterval循环中调用此功能以检测每个帧上的对象:

object_detector.js

interval = setInterval(async() => {
    context.drawImage(video,0,0);
    const input = prepare_input(canvas);
    const output = await run_model(input)
},30)

请注意,我在调用run_model时添加了setIntervalawait关键字的功能的async关键字,因为这是一个异步函数,需要一些时间才能完成执行。

要使其正常工作,您需要在某些HTTP服务器中运行index.html,例如在VS代码的嵌入式Web服务器中,因为Run_Model函数需要使用http。

yolov8n.onnx文件下载到浏览器中。

现在是时候将RAW YOLOV8模型输出转换为已检测到的对象的边界框的时候了。

处理输出

您只需从上一篇文章的appropriate section复制process_output函数。

object_detector.js

function process_output(output, img_width, img_height) {
    let boxes = [];
    for (let index=0;index<8400;index++) {
        const [class_id,prob] = [...Array(80).keys()]
            .map(col => [col, output[8400*(col+4)+index]])
            .reduce((accum, item) => item[1]>accum[1] ? item : accum,[0,0]);
        if (prob < 0.5) {
            continue;
        }
        const label = yolo_classes[class_id];
        const xc = output[index];
        const yc = output[8400+index];
        const w = output[2*8400+index];
        const h = output[3*8400+index];
        const x1 = (xc-w/2)/640*img_width;
        const y1 = (yc-h/2)/640*img_height;
        const x2 = (xc+w/2)/640*img_width;
        const y2 = (yc+h/2)/640*img_height;
        boxes.push([x1,y1,x2,y2,label,prob]);
    }

    boxes = boxes.sort((box1,box2) => box2[5]-box1[5])
    const result = [];
    while (boxes.length>0) {
        result.push(boxes[0]);
        boxes = boxes.filter(box => iou(boxes[0],box)<0.7);
    }
    return result;
}

此外,复制用于实现可可对象类标签的“联合交叉”算法和数组的助手功能:

object_detector.js

function iou(box1,box2) {
    return intersection(box1,box2)/union(box1,box2);
}

function union(box1,box2) {
    const [box1_x1,box1_y1,box1_x2,box1_y2] = box1;
    const [box2_x1,box2_y1,box2_x2,box2_y2] = box2;
    const box1_area = (box1_x2-box1_x1)*(box1_y2-box1_y1)
    const box2_area = (box2_x2-box2_x1)*(box2_y2-box2_y1)
    return box1_area + box2_area - intersection(box1,box2)
}

function intersection(box1,box2) {
    const [box1_x1,box1_y1,box1_x2,box1_y2] = box1;
    const [box2_x1,box2_y1,box2_x2,box2_y2] = box2;
    const x1 = Math.max(box1_x1,box2_x1);
    const y1 = Math.max(box1_y1,box2_y1);
    const x2 = Math.min(box1_x2,box2_x2);
    const y2 = Math.min(box1_y2,box2_y2);
    return (x2-x1)*(y2-y1)
}

const yolo_classes = [
    'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat',
    'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse',
    'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase',
    'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard',
    'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
    'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant',
    'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven',
    'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'
];

在这里,我在可可数据集上使用了标签阵列进行验证的模型。如果您使用其他模型,则标签显然应该不同。

最后,为setInterval循环中的每个视频框架调用此功能:

object_detector.js

interval = setInterval(async() => {
        context.drawImage(video,0,0);
        const input = prepare_input(canvas);
        const output = await run_model(input);
        const boxes = process_output(output, canvas.width, canvas.height);
    },30)

process_output功能接收了原始模型输出和画布的尺寸,以将边界框扩展到原始图像大小。 (请记住,该模型可与640x640图像一起使用)。

最后,盒子数组包含每个检测到的对象的边界框:[x1,y1,x2,y2,label,prob]。

剩下要做的就是在画布上的图像顶部绘制这些盒子。

绘制边界框

现在,您需要编写一个使用HTML5 Canvas上下文API的函数,以使用对象类标签为每个边界框绘制矩形。您可以重复使用上一篇文章中每个项目中编写的draw_image_and_boxes函数。这就是原始功能的外观:

function draw_image_and_boxes(file,boxes) {
    const img = new Image()
    img.src = URL.createObjectURL(file);
    img.onload = () => {
        const canvas = document.querySelector("canvas");
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext("2d");
        ctx.drawImage(img,0,0);
        ctx.strokeStyle = "#00FF00";
        ctx.lineWidth = 3;
        ctx.font = "18px serif";
        boxes.forEach(([x1,y1,x2,y2,label]) => {
            ctx.strokeRect(x1,y1,x2-x1,y2-y1);
            ctx.fillStyle = "#00ff00";
            const width = ctx.measureText(label).width;
            ctx.fillRect(x1,y1,width+10,25);
            ctx.fillStyle = "#000000";
            ctx.fillText(label, x1, y1+18);
        });
    }
}

但是,您可以简化它,因为在这种情况下,您不需要从文件加载图像,然后在画布上显示,因为您已经在画布上显示了图像。您只需要将画布传递给此功能,然后在其上绘制盒子即可。另外,将功能重命名为draw_boxes,因为图像已经在输入画布上绘制。这就是您可以修改它的方式:

object_detector.js

function draw_boxes(canvas,boxes) {
    const ctx = canvas.getContext("2d");
    ctx.strokeStyle = "#00FF00";
    ctx.lineWidth = 3;
    ctx.font = "18px serif";
    boxes.forEach(([x1,y1,x2,y2,label]) => {
        ctx.strokeRect(x1,y1,x2-x1,y2-y1);
        ctx.fillStyle = "#00ff00";
        const width = ctx.measureText(label).width;
        ctx.fillRect(x1,y1,width+10,25);
        ctx.fillStyle = "#000000";
        ctx.fillText(label, x1, y1+18);
    });
}
  • 该函数以当前帧接收canvas,并在其上接收boxes阵列。
  • 功能设置填充,中风和字体样式。
  • 然后它穿越boxes数组。它绘制每个检测到的对象和类标签周围的绿色边界矩形。要显示类标签,它使用黑色文本和绿色背景。

现在,您可以在setInterval循环中为每个帧调用此功能:

object_detector.js

interval = setInterval(async() => {
     context.drawImage(video,0,0);
     const input = prepare_input(canvas);
     const output = await run_model(input);
     const boxes = process_output(output, canvas.width, canvas.height);
     draw_boxes(canvas,boxes)
 },30)

但是,以这种方式编写的代码无法正常工作。 draw_boxes是周期中的最后一行,因此,在此行之后,下一个迭代将启动并覆盖context.drawImage(video, 0,0, canvas.width, canvas.height)行所显示的框。因此,您将永远不会看到显示的框。您需要首先使用drawImage,而draw_boxes接下来需要,但是当前代码将以相反的顺序进行。我们将使用以下技巧来修复它:

object_detector.js

let interval
let boxes = [];
video.addEventListener("play", async() => {
    const canvas = document.querySelector("canvas");
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const context = canvas.getContext("2d");
    interval = setInterval(async() => {
        context.drawImage(video,0,0);
        draw_boxes(canvas, boxes);
        const input = prepare_input(canvas);
        const output = await run_model(input);
        boxes = process_output(output, canvas.width, canvas.height);
    },30)
});

在此代码段中,我在“播放”事件处理程序之前将boxes宣布为全局变量。默认情况下,这是一个空数组。这样,您可以在使用drawImage函数的画布上绘制视频框架后立即运行draw_boxes功能。在第一次迭代中,它不会在图像顶部绘制任何内容,但是它将运行模型并用检测到的对象覆盖boxes数组。然后,它将在下一个迭代的开头绘制检测到的对象的边界框。假设您每30毫秒进行迭代,则以前和当前帧之间的差异不会显着。

最后,如果一切正确实现,您将看到带有围绕检测到对象的边界框的视频。

Image description

也许当您运行此操作时,您会在视频中遇到烦人的延迟。 run_model功能中的机器学习模型推断是CPU密集型操作,可能需要比30毫秒更多的时间。这就是为什么它打断视频的原因。延迟持续时间取决于您的CPU功率。幸运的是,有一种方法可以修复它,我们将在下面介绍。

在JavaScript中并行运行多个任务

默认情况下,JavaScript是单线螺纹。它具有主线程,或者有时称为UI线程。您所有的代码都在其中运行。但是,通过CPU密集型任务(例如机器学习模型执行)中断UI并不是一个好习惯。您应该将CPU密集任务移动到分开线程以不阻止用户界面。

在JavaScript中创建线程的一种常见方法是使用WebWorkers API。使用此API,您可以创建一个Worker对象并将JavaScript文件传递给它,例如在此代码中:

const worker = new Worker("worker.js");

worker对象将在单独的线程中运行worker.js文件。此文件中的所有代码都将与用户界面并行运行。

以这种方式产生的工作线程无法访问网页元素或在其中定义的任何代码。主线程相同,它无法访问Worker线程的内容。为了在线程之间进行通信,网络工作人员API使用消息。您可以将带有数据的消息发送到线程,并从中收听消息。

Worker线程可以做同样的事情:它可以将消息发送到主线程,并从主线程中收听消息。以这种方式定义的通信是异步的。

例如,要向您之前创建的工作线程发送消息,您应该运行:

worker.postMessage(data)

data参数是任何JavaScript对象。

要从工作线程中收听消息,您需要定义onmessage事件处理程序:

worker.onmessage = (event) => {
    console.log(event.data);
};

消息来自工人时,它会触发功能并传递event参数内的传入消息。 event.data属性包含数据,该数据是使用postMessage函数发送的worker线程。

您可以在documentation中阅读有关网络工作人员的更多理论,然后我们将进行练习。

让我们通过视频延迟解决问题。我们拥有的时间和资源消耗功能最多的是run_model。因此,我们将其移至新的工作人员线程。然后,我们将将input发送到此线程,并将从中接收output。在视频播放时,它将在每个帧的背景中工作。

在背景线程中运行模型

让我们创建一个worker.js文件,然后将运行模型的代码移动到此文件中:

worker.js

importScripts("https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js");

async function run_model(input) {
    const model = await ort.InferenceSession.create("./yolov8n.onnx");
    input = new ort.Tensor(Float32Array.from(input),[1, 3, 640, 640]);
    const outputs = await model.run({images:input});
    return outputs["output0"].data;
}

第一行导入ONNX运行时JavaScript API库,因为如前所述,Worker线程无法访问网页以及在其中导入的任何内容。 importScripts功能用于将外部脚本导入工作线程。

此处导入的JavaScript ONNX API库仅包含高级JavaScript函数,但不包含ONNX运行时库本身。 JavaScript的onnx运行时库是原始ONNX运行时的WebAssembly编译,该编辑是用C写成的。不是,它会自动将ort-wasm-simd.wasm文件下载到您的Web浏览器中。我遇到了一个问题。如果从Web Worker运行此文件,则不会下载此文件。我认为,最好的快速解决方法是从存储库中手动下载ort-wasm-simd.wasm文件并将其放在项目文件夹中。

之后,我从object_detector.js复制/粘贴了run_model函数。

现在,我们需要从所有其他代码工作的主UI线程中将input发送到此脚本。为此,我们需要在object_detector.js中创建一个新的工人。您可以在开始时执行此操作:

object_detector.js

const worker = new Worker("worker.js")

然后,您需要发布一条消息,而不是run_model呼叫。

object_detector.js

interval = setInterval(() => {
        context.drawImage(video,0,0, canvas.width, canvas.height);
        draw_boxes(canvas, boxes);
        const input = prepare_input(canvas);
        worker.postMessage(input);
//        boxes = process_output(output, canvas.width, canvas.height);
    },30)

在这里,我使用postMessage函数将input发送给了工人,并在其之后对所有代码进行了评论,因为我们只有在工作后处理input并返回output之后才运行它。您可以删除此行,因为它将在以后在其他功能中使用,我们将在其中处理Worker线程的消息。

让我们现在回到工人。它应该收到您发送的输入。要接收消息,您需要定义onmessage处理程序。让我们将其添加到worker.js

worker.js

onmessage = async(event) => {
    const input = event.data;
    const output = await run_model(input);
    postMessage(output);
}

这是应在工作线程中实现来自主线程的消息的事件处理程序。处理程序定义为异步函数。消息到来时,它将数据从消息提取到input变量。然后,它使用此输入调用run_model。最后,它使用postMessage函数将output从模型发送到主线程。

在这里,请务必牢记,主要线程之间的消息流动在异步方面,因此,主线程不会等到工作线程完成的run_model,并将继续每30次发送新框架到工作线程毫秒。它可能会导致巨大的请求队列,尤其是如果用户的CPU缓慢。我建议忽略对run_model的所有新请求,直到与当前的请求一起使用。这可以通过以下方式实现:

worker.js

let busy = false;
onmessage = async(event) => {
    if (busy) {
        return
    }
    busy = true;
    const input = event.data;
    const output = await run_model(input);
    postMessage(output);
    busy = false;
}

在这里,我定义了一个busy变量,该变量充当信号量。当新消息到达时,处理程序将busy变量设置为true,以表示消息处理开始。然后,将忽略所有后续请求,直到处理此请求并将busy变量的值重置为false

模型返回output并将其发送到主线程作为消息时,主线程应接收此消息并处理模型的输出。为此,您需要为object_detector.js中的worker线程定义onmessage处理程序。

object_detector.js

worker.onmessage = (event) => {
    const output = event.data;
    const canvas = document.querySelector("canvas");
    boxes =  process_output(output, canvas.width, canvas.height);
};

在这里,当模型的输出来自工作线程时,您可以使用process_output函数对其进行处理,然后将其保存到boxes全局变量。因此,新盒子将可以绘制。

我们定义的过程将与主要视频播放循环并行工作。这是object_detector.js的完整来源:

object_detector.js

const video = document.querySelector("video");

const worker = new Worker("worker.js");
let boxes = [];
let interval
video.addEventListener("play", () => {
    const canvas = document.querySelector("canvas");
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const context = canvas.getContext("2d");
    interval = setInterval(() => {
        context.drawImage(video,0,0);
        draw_boxes(canvas, boxes);
        const input = prepare_input(canvas);
        worker.postMessage(input);
    },30)
});

worker.onmessage = (event) => {
    const output = event.data;
    const canvas = document.querySelector("canvas");
    boxes =  process_output(output, canvas.width, canvas.height);
};

video.addEventListener("pause", () => {
    clearInterval(interval);
});

const playBtn = document.getElementById("play");
const pauseBtn = document.getElementById("pause");
playBtn.addEventListener("click", () => {
    video.play();
});
pauseBtn.addEventListener("click", () => {
    video.pause();
});

function prepare_input(img) {
    const canvas = document.createElement("canvas");
    canvas.width = 640;
    canvas.height = 640;
    const context = canvas.getContext("2d");
    context.drawImage(img, 0, 0, 640, 640);
    const data = context.getImageData(0,0,640,640).data;
    const red = [], green = [], blue = [];
    for (let index=0;index<data.length;index+=4) {
        red.push(data[index]/255);
        green.push(data[index+1]/255);
        blue.push(data[index+2]/255);
    }
    return [...red, ...green, ...blue];
}

function process_output(output, img_width, img_height) {
    let boxes = [];
    for (let index=0;index<8400;index++) {
        const [class_id,prob] = [...Array(80).keys()]
            .map(col => [col, output[8400*(col+4)+index]])
            .reduce((accum, item) => item[1]>accum[1] ? item : accum,[0,0]);
        if (prob < 0.5) {
            continue;
        }
        const label = yolo_classes[class_id];
        const xc = output[index];
        const yc = output[8400+index];
        const w = output[2*8400+index];
        const h = output[3*8400+index];
        const x1 = (xc-w/2)/640*img_width;
        const y1 = (yc-h/2)/640*img_height;
        const x2 = (xc+w/2)/640*img_width;
        const y2 = (yc+h/2)/640*img_height;
        boxes.push([x1,y1,x2,y2,label,prob]);
    }
    boxes = boxes.sort((box1,box2) => box2[5]-box1[5])
    const result = [];
    while (boxes.length>0) {
        result.push(boxes[0]);
        boxes = boxes.filter(box => iou(boxes[0],box)<0.7 || boxes[0][4] !== box[4]);
    }
    return result;
}

function iou(box1,box2) {
    return intersection(box1,box2)/union(box1,box2);
}

function union(box1,box2) {
    const [box1_x1,box1_y1,box1_x2,box1_y2] = box1;
    const [box2_x1,box2_y1,box2_x2,box2_y2] = box2;
    const box1_area = (box1_x2-box1_x1)*(box1_y2-box1_y1)
    const box2_area = (box2_x2-box2_x1)*(box2_y2-box2_y1)
    return box1_area + box2_area - intersection(box1,box2)
}

function intersection(box1,box2) {
    const [box1_x1,box1_y1,box1_x2,box1_y2] = box1;
    const [box2_x1,box2_y1,box2_x2,box2_y2] = box2;
    const x1 = Math.max(box1_x1,box2_x1);
    const y1 = Math.max(box1_y1,box2_y1);
    const x2 = Math.min(box1_x2,box2_x2);
    const y2 = Math.min(box1_y2,box2_y2);
    return (x2-x1)*(y2-y1)
}

function draw_boxes(canvas,boxes) {
    const ctx = canvas.getContext("2d");
    ctx.strokeStyle = "#00FF00";
    ctx.lineWidth = 3;
    ctx.font = "18px serif";
    boxes.forEach(([x1,y1,x2,y2,label]) => {
        ctx.strokeRect(x1,y1,x2-x1,y2-y1);
        ctx.fillStyle = "#00ff00";
        const width = ctx.measureText(label).width;
        ctx.fillRect(x1,y1,width+10,25);
        ctx.fillStyle = "#000000";
        ctx.fillText(label, x1, y1+18);
    });
}

const yolo_classes = [
    'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat',
    'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse',
    'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase',
    'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard',
    'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
    'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant',
    'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven',
    'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'
];

这是工作线程代码:

worker.js

importScripts("https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js");

let busy = false;
onmessage = async(event) => {
    if (busy) {
        return
    }
    busy = true;
    const input = event.data;
    const output = await run_model(input);
    postMessage(output);
    busy = false;
}

async function run_model(input) {
    const model = await ort.InferenceSession.create("./yolov8n.onnx");
    input = new ort.Tensor(Float32Array.from(input),[1, 3, 640, 640]);
    const outputs = await model.run({images:input});
    return outputs["output0"].data;
}

另外,您可以从index.html中删除onnx运行时库,因为它是在worker.js中导入的。这是最终的index.html文件:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Object detector</title>
</head>
<body>
<video controls style="display:none" src="sample.mp4"></video>
<br/>
<canvas></canvas><br/>
<button id="play">Play</button>&nbsp;
<button id="pause">Pause</button>
<script src="object_detector.js" defer></script>
</body>
</html>

如果您现在在Web服务器中运行index.html文件,则应看到以下结果。

结论

在本文中,我展示了如何使用Web浏览器中的Yolov8神经网络在视频中检测对象,而无需任何后端。我们使用

此外,我们发现了如何使用网络工人在JavaScript中并行运行多个任务。这样,我们将机器学习模型执行代码移至背景线程,以免使用此CPU密集任务中断用户界面。

您可以在this repository中找到本文的完整源代码。

使用算法,在本文中解释了您不仅可以在视频文件中检测对象,而且可以在其他视频源中检测到对象,例如,例如在网上摄像机的视频中。您需要在此项目中更改的所有内容都是将网络摄像头设置为

本文中创建的项目不是完整的生产就绪解决方案。这里有很多要改进的地方。例如,如果使用对象跟踪算法的工作速度更快,则可以提高速度和准确性。您可以仅运行神经网络来检测相同的对象,而是只能为第一个帧运行它,以获取初始对象位置,然后使用对象跟踪算法来跟踪随后帧上的检测到的边界框。阅读有关对象跟踪方法here的更多信息。我将在下一篇文章中写更多有关此的信息。

LinkedInTwitterFacebook上关注我,首先了解此类新文章和其他软件开发新闻。

有一个有趣的编码,永不停止学习!