如何使用Yolov8神经网络实现实例细分
#javascript #网络开发人员 #ai #computervision

目录

Introduction
Getting started with YOLOv8 segmentation
Train the YOLOv8 model for image segmentation
Using YOLOv8 segmentation model in production
Export the YOLOv8 segmentation model to ONNX
Load the model using ONNX
吃了abiaoaqian8
Run the model
Abiaoooqian10

Epiaoqian12
- sooooqi13
© - sooooqu14
】吃了abiaoooo115
Create a segmentation web application
吃了abiaooqian17
吃了abiaooqian18
Conclusion

介绍

这是我Yolov8系列的第四部分。在以前的文章中,我描述了如何使用Yolov8使用不同编程语言在图像和视频中检测对象。但是,Yolov8还可以使用实例分割来更精确地检测对象。

对象检测的结果是所有检测到的对象周围的边界框列表。实例分割的结果是每个检测到的对象的分割掩码。分割面膜是一个黑白图像,所有属于对象的像素都是白色的,所有其他像素都是黑色的,如下一个图像上显示:

Image description

拥有此掩码后,实际上是一个具有值为0的2D矩阵或对象像素的255,您可以将其应用于图像以仅绘制图像的像素的图像的像素面具。这样,您可以从图像中删除背景:

Image description

并为对象设置新背景:

Image description

以相同的方式,您可以为每个视频的每个帧运行实例分割,并从整个视频中删除背景。

此外,拥有这些检测到的对象的掩模,您可以计算它们的轮廓。

Image description

计算出的轮廓是多边形,它是点坐标的数组。该多边形可用于识别现实世界应用程序中图像的检测对象,而不是仅使用边界框。

用于自动驾驶汽车,医学想象,空中作物监测等的实例分割。

例如,众所周知的[Chromacam]应用程序(https://www.chromacam.me/)及其类似物使用图像分割来检测您的形状和边界,并在您从网络摄像头传输视频时替换或模糊背景。

在本文中,我将指导您如何使用yolov8实现图像的实例细分。首先,我们将使用默认的Ultrytics API,其中大多数内部工作都大大自动化,我们将使用带有Yolov8的验证模型,该模型检测COCO dataset的80个对象类。然后,我将进行审查如何准备数据并在其上训练模型以检测自定义对象类,这是特定业务任务所需的。然后,在使用高级API之后,我们将潜入内部内容,并准备Yolov8型号的输入,并通过手来解析其输出。最后,我们将创建一个Web应用程序,您可以在其中上传图像,将其通过Yolov8模型并显示所有对象的轮廓,并在其上检测到。

yolov8分割模型基于我在先前文章中介绍的对象检测模型。当您进行实例分割时,模型会自动进行对象检测并返回对象检测的结果和实例分割的结果。这就是为什么重要的是阅读我以前的本系列文章,至少是firstsecond零件,因为我将重复使用这些帖子中的大量代码。

Yolov8细分入门

让我们开始编码。当您可以运行Python代码时,您需要一个环境。我建议使用Jupyter Notebook。在下一节中,所有代码示例和输出都假定您在jupyter笔记本中运行此操作。

确保通过在笔记本中运行以下命令安装的Ultrytics软件包:

!pip install ultralytics

然后,导入YOLO模型工厂对象:

from ultralytics import YOLO

然后,您需要实例化模型,该模型将用于预测。我将使用带有Yolov8的验证的中型型号,该模型可以检测80个对象类。与对象检测模型相反,分割模型名称具有-seg后缀。因此,如果您需要加载中型模型进行分割,则需要指定yolov8m-seg.pt文件。

model = YOLO("yolov8m-seg.pt")

另外,所有相同的分割模型可用:yolov8n-seg.ptyolov8s-seg.ptyolov8m-seg.ptyolov8t-seg.ptyolov8x-seg.pt

对于所有示例,我将与猫和狗一起使用图像,该图像命名为cat_dog.jpg,并带有笔记本的当前文件夹:

Image description

让我们运行模型以获取此图像的分割结果:

results = model.predict("cat_dog.jpg")

用于分割模型的predict方法与对象检测模型相同。它返回方法调用中指定的每个图像的结果数组。在这种情况下,此数组包含一个项目。让我们来吧:

result = results[0]

此外,分割模型也运行对象检测。因此,返回的result几乎与我们在本系列的the first article中运行对象检测时几乎相同。另外,它具有masks属性,该属性是检测到的对象分割掩码的数组。

让我们看看检测到多少个口罩:

masks = result.masks
len(masks)
2

结果是可以预测的,它分割了2个对象:狗和猫。让我们获得以下面具的第一个:

mask1 = masks[0]

每个掩码是具有一组属性的对象。我们将使用其中两个:

  • data-对象的分割面膜,即黑白图像矩阵,其中0个元素是黑色像素,1个元素是白色像素。
  • xy-对象的多边形,这是点的数组。

还有其他属性。您可以在official documentation

中学到的所有知识

data属性包裹在pytorch张量阵列中,但是我认为,与numpy阵列一样,使用它会更常见。让我们提取口罩和多边形:

mask = mask1.data[0].numpy()
polygon = mask1.xy[0]

让我们显示第一个对象的掩码。我们将使用Pillow图像对象。它具有从numpy矩阵加载图像的fromarray方法。确保安装了Pillow

!pip install pillow

并从面具中创建图像:

from PIL import Image
mask_img = Image.fromarray(mask,"I")
mask_img

它应该显示以下图像:

Image description

所以,您会发现这是dog的分割面具。

现在,让我们使用polygon对象。显示它以查看点的数组:

polygon
array([[     280.18,      96.575],
       [      275.4,      101.36],
       [      275.4,      102.31],
       [     274.44,      103.27],
       [     274.44,      105.18],
       [     273.49,      106.14],
       [     273.49,      107.09],
       [     272.53,      108.05],
       [     272.53,      111.87],
       [     271.57,      112.83],
       [     271.57,      117.61],
       [     272.53,      118.57],
       [     272.53,      152.04]
...
       [     302.17,      112.83],
       [     302.17,      110.92],
       [     301.22,      109.96],
       [     301.22,      108.05],
       [     298.35,      105.18],
       [     298.35,      104.22],
       [     297.39,      103.27],
       [     297.39,      102.31],
       [     292.61,      97.531],
       [     291.66,      97.531],
       [      290.7,      96.575]], dtype=float32)

每个点是带有坐标[x,y]的列表。您可以对此做任何想做的事情。例如,您可以将其绘制在cat_dog图像的顶部:

from PIL import ImageDraw

img = Image.open("cat_dog.jpg")
draw = ImageDraw.Draw(img)
draw.polygon(polygon,outline=(0,255,0), width=5)
img

此代码从枕头上导入ImageDraw模块,该模块用于绘制图像顶部。然后,它打开cat_dog.jpg映像,并用它初始化draw对象。然后,它使用polygon点在其上绘制多边形。 outline参数指定线颜色(绿色),而width指定线宽度。最后,您应该看到带有概述的狗的图像:

Image description

现在,总结这一点,让我们对第二个掩码做同样的事情:

mask2 = masks[1]
mask = mask2.data[0].numpy()
polygon = mask2.xy[0]
mask_img = Image.fromarray(mask,"I")
mask_img

Image description

draw.polygon(polygon,outline=(0,255,0), width=5)
img

Image description

为方便起见,您可以将本章的整个编码会议视为视频。

使用在众所周知的对象上预测的模型可以启动,但是实际上,您可能需要一个解决方案来针对具体业务问题进行特定对象。

例如,有人可能需要在超市货架上检测特定产品或在X射线上发现脑肿瘤。这些信息很有可能在公共数据集中不可用,也没有任何了解所有内容的模型。

因此,您必须教自己的模型来检测这些类型的对象。为此,您需要为问题创建一个带注释的图像的数据库,并在这些图像上训练模型。

训练Yolov8模型进行图像分割

要训练模型,您需要准备带注释的图像并将其拆分为培训和验证数据集。培训集将用于教授模型,验证集将用于测试本研究的结果,以测量训练有素的模型的质量。您可以将80%的图像放入培训集中,并将20%的图像放在验证集中。

这些是您需要遵循的步骤来创建每个数据集:

  1. 决定要教您的模型检测的对象的类和编码类。例如,如果您只想检测猫和狗,则可以说“ 0”是猫,而“ 1”是狗。

  2. 为您的数据集创建一个文件夹,其中有两个子文件夹:“图像”和“标签”。

  3. 将图像放在“图像”子文件夹中。您收集的图像越多,训练越好。

  4. 对于每个图像,在“标签”子文件夹中创建一个注释文本文件。注释文本文件应具有与图像文件和“ .txt”扩展名相同的名称。在注释文件中,您应该添加有关每个对象的记录,这些记录以以下格式存在于适当的图像上:

{object_class_id} {polygon}
  • object_class_id是对象类的标签,例如0是猫,如果是狗,则是1个。
  • polygon是以下格式为该对象界限多边形的坐标:x1 y1 x2 y2 ...

实际上,这是机器学习过程中最耗时的手动工作:测量所有对象的边界多边形的坐标并将其添加到注释文件中。此外,坐标应为归一化以适合0到1的范围。要计算它们,您需要使用以下公式:

x = x/image_width
y = y/image_height

因此,如果您具有(100,100)坐标的点,并且图像大小为(612,415),那么,请使用以下来计算注释:

x = 100/612 = 0.163666121
y = 100/415 = 0.240963855

这样,您需要为每个图像上的所有对象设置多边形。例如,如果您的图像具有以下猫和狗多边形:

Image description

然后,您需要为其创建以下注释文件:

1 0.45781 0.23271 0.45 0.24423 0.45 0.24654 0.44844 0.24884 0.44844 0.25345 0.44687 0.25575 0.44687 0.25806 0.44531 0.26036 0.44531 0.26958 0.44375 0.27188 0.44375 0.2834 0.44531 0.28571 0.44531 0.36636 0.44375 0.36866 0.44375 0.38018 0.44219 0.38248 0.44219 0.3894 0.44062 0.3917 0.44062 0.42857 0.43906 0.43087 0.43906 0.45622 0.44062 0.45852 0.44062 0.48157 0.43906 0.48387 0.43906 0.49309 0.4375 0.49539 0.4375 0.50461 0.43594 0.50691 0.43594 0.51843 0.43437 0.52074 0.43437 0.52765 0.43281 0.52995 0.43281 0.54148 0.43125 0.54378 0.43125 0.58295 0.43281 0.58526 0.43281 0.58986 0.43437 0.59217 0.43437 0.59447 0.4375 0.59908 0.4375 0.60139 0.44219 0.6083 0.44219 0.6106 0.44375 0.61291 0.44375 0.61521 0.44531 0.61752 0.44531 0.61982 0.45156 0.62904 0.45156 0.63134 0.46875 0.65669 0.47031 0.65669 0.47344 0.6613 0.47344 0.6636 0.475 0.6659 0.475 0.67512 0.47344 0.67742 0.47344 0.69816 0.475 0.70047 0.475 0.71199 0.47656 0.71429 0.47656 0.7166 0.48437 0.72812 0.4875 0.72812 0.48906 0.72581 0.49062 0.72581 0.49375 0.7212 0.49375 0.7166 0.49844 0.70968 0.49844 0.70738 0.50156 0.70277 0.50312 0.70277 0.50469 0.70047 0.50937 0.70047 0.51406 0.70738 0.51406 0.70968 0.51562 0.71199 0.51562 0.71429 0.51719 0.7166 0.51562 0.7189 0.51562 0.72351 0.51719 0.72581 0.51875 0.72581 0.52031 0.72812 0.52969 0.72812 0.53281 0.72351 0.54531 0.72351 0.54687 0.72581 0.55156 0. 72581 0.55625 0.73273 0.55781 0.73273 0.55937 0.73503 0.56094 0.73273 0.56875 0.73273 0.57031 0.73503 0.575 0.73503 0.57656 0.73273 0.57812 0.73503 0.58281 0.73503 0.58437 0.73733 0.59375 0.73733 0.59531 0.73964 0.6 0.73964 0.60156 0.74194 0.625 0.74194 0.62656 0.73964 0.63281 0.73964 0.63437 0.73733 0.63906 0.73733 0.64062 0.73503 0.64219 0.73503 0.64375 0.73273 0.64531 0.73273 0.64687 0.73042 0.64844 0.73042 0.65 0.72812 0.65156 0.72812 0.65312 0.72581 0.65469 0.72581 0.65625 0.72351 0.65781 0.72351 0.65937 0.7212 0.6625 0.7212 0.66406 0.7189 0.66719 0.7189 0.66875 0.7166 0.67187 0.7166 0.67344 0.71429 0.67969 0.71429 0.68125 0.71199 0.6875 0.71199 0.68906 0.70968 0.70156 0.70968 0.70312 0.71199 0.70469 0.71199 0.70781 0.7166 0.71094 0.7166 0.7125 0.7189 0.71562 0.7189 0.71719 0.7212 0.71875 0.7212 0.72031 0.72351 0.72656 0.72351 0.72812 0.72581 0.73437 0.72581 0.73594 0.72351 0.7375 0.72351 0.74219 0.7166 0.74219 0.71429 0.74375 0.71199 0.74375 0.70738 0.74531 0.70508 0.74531 0.70047 0.74687 0.69816 0.74687 0.69125 0.74844 0.68895 0.74844 0.67742 0.75 0.67512 0.75 0.65208 0.75156 0.64977 0.75156 0.64056 0.75 0.63825 0.75 0.62904 0.74844 0.62673 0.74844 0.61752 0.74687 0.61521 0.74687 0.6106 0.74531 0.6083 0.74531 0.60599 0.74375 0.60369 0.74375 0.60139 0.74219 0.59908 0.74219 0.59447 0.74062 0.59217 0.74062 0.58986 0.7375 0.58526 0.7375 0.58065 0.73594 0.57834 0.73594 0.57373 0.73281 0.56913 0.73281 0.56682 0.72969 0.56221 0.72969 0.55991 0.72812 0.55761 0.72812 0.5553 0.725 0.55069 0.725 0.54839 0.72031 0.54148 0.72031 0.53917 0.71562 0.53226 0.71562 0.52995 0.71406 0.52995 0.70156 0.51152 0.7 0.51152 0.69844 0.50922 0.69687 0.50922 0.69531 0.50691 0.69219 0.50691 0.69062 0.50461 0.67969 0.50461 0.67812 0.5023 0.67031 0.5023 0.66875 0.50461 0.66094 0.50461 0.65937 0.50691 0.65156 0.50691 0.65 0.50922 0.625 0.50922 0.62344 0.50691 0.62031 0.50691 0.61875 0.50461 0.61875 0.5023 0.61562 0.4977 0.61562 0.49539 0.6125 0.49078 0.61094 0.49078 0.60312 0.47926 0.60312 0.47696 0.6 0.47235 0.6 0.46774 0.59844 0.46544 0.59844 0.46083 0.59687 0.45852 0.59687 0.45392 0.59531 0.45161 0.59531 0.44931 0.59375 0.447 0.59375 0.44009 0.59219 0.43779 0.59219 0.42396 0.59062 0.42166 0.59062 0.40092 0.58906 0.39861 0.58906 0.39631 0.5875 0.39401 0.5875 0.38709 0.58594 0.38479 0.58594 0.29723 0.5875 0.29492 0.5875 0.26036 0.58594 0.25806 0.58594 0.25114 0.58437 0.24884 0.58437 0.24654 0.57656 0.23502 0.57344 0.23502 0.57187 0.23271 0.57031 0.23271 0.56875 0.23502 0.56406 0.23502 0.55156 0.25345 0.55156 0.25575 0.55 0.25806 0.55 0.26036 0.54844 0.26267 0.54844 0.26497 0.54531 0.26958 0.54531 0.27188 0.54375 0.27419 0.54375 0.27649 0.54219 0.2788 0.54219 0.2834 0.5375 0.29032 0.5375 0.29262 0.53437 0.29723 0.53125 0.29723 0.52969 0.29953 0.51562 0.29953 0.51406 0.29723 0.50937 0.29723 0.50781 0.29492 0.50625 0.29492 0.49844 0.2834 0.49844 0.2811 0.49687 0.2788 0.49687 0.27649 0.49375 0.27188 0.49375 0.26727 0.49219 0.26497 0.49219 0.26036 0.4875 0.25345 0.4875 0.25114 0.48594 0.24884 0.48594 0.24654 0.47812 0.23502 0.47656 0.23502 0.475 0.23271
0 0.25 0.41705 0.24844 0.41935 0.24687 0.41935 0.24531 0.42166 0.24531 0.42857 0.24375 0.43087 0.24375 0.46774 0.24219 0.47005 0.24219 0.48157 0.24062 0.48387 0.24062 0.48848 0.23906 0.49078 0.23906 0.4977 0.2375 0.5 0.2375 0.50922 0.23594 0.51152 0.23594 0.52074 0.23437 0.52304 0.23437 0.52995 0.23281 0.53226 0.23281 0.54378 0.23125 0.54608 0.23125 0.63825 0.23281 0.64056 0.23281 0.65208 0.23437 0.65438 0.23437 0.65669 0.23594 0.65899 0.23594 0.6636 0.2375 0.6659 0.2375 0.67512 0.23906 0.67742 0.23906 0.68434 0.24062 0.68664 0.24062 0.69125 0.24219 0.69355 0.24219 0.70047 0.24375 0.70277 0.24375 0.70968 0.24531 0.71199 0.24531 0.7166 0.25 0.72351 0.25 0.72581 0.25156 0.72812 0.25625 0.72812 0.25781 0.73042 0.2875 0.73042 0.28906 0.73273 0.29219 0.73273 0.29375 0.73503 0.29687 0.73503 0.29844 0.73733 0.30312 0.73733 0.30469 0.73964 0.30781 0.73964 0.30937 0.74194 0.3125 0.74194 0.31406 0.74425 0.31562 0.74425 0.31875 0.74886 0.32031 0.74886 0.32187 0.75116 0.32344 0.75116 0.325 0.75346 0.3375 0.75346 0.33906 0.75116 0.35312 0.75116 0.35469 0.75346 0.36094 0.75346 0.3625 0.75577 0.37031 0.75577 0.37187 0.75807 0.37812 0.75807 0.37969 0.75577 0.38594 0.75577 0.3875 0.75346 0.40781 0.75346 0.4125 0.74655 0.4125 0.74425 0.41562 0.73964 0.41562 0.7166 0.41406 0.71429 0.41406 0.71199 0.40937 0.70508 0.40937 0.70277 0.40469 0.69586 0.40469 0.68895 0.40312 0.68664 0.40312 0.67742 0.40156 0.67512 0.40156 0.66821 0.4 0.6659 0.4 0.6106 0.39844 0.6083 0.39844 0.59447 0.39687 0.59217 0.39687 0.58756 0.39531 0.58526 0.39531 0.58295 0.39375 0.58065 0.39375 0.57604 0.39219 0.57373 0.39219 0.57143 0.39062 0.56913 0.39062 0.56682 0.38906 0.56452 0.38906 0.56221 0.3875 0.55991 0.3875 0.55761 0.38594 0.5553 0.38594 0.55069 0.38437 0.54839 0.38437 0.54608 0.38125 0.54148 0.38125 0.53917 0.37812 0.53456 0.37812 0.53226 0.36875 0.51843 0.36719 0.51843 0.36406 0.51383 0.3625 0.51383 0.35937 0.50922 0.35781 0.50922 0.35625 0.50691 0.35312 0.50691 0.35 0.5023 0.34844 0.5023 0.34687 0.5 0.34375 0.5 0.34219 0.4977 0.34062 0.4977 0.33125 0.48387 0.33125 0.47926 0.32969 0.47696 0.32969 0.46083 0.33125 0.45852 0.33125 0.447 0.33281 0.4447 0.33281 0.42396 0.33437 0.42166 0.33281 0.41935 0.32969 0.41935 0.32812 0.41705 0.32656 0.41705 0.325 0.41935 0.32187 0.41935 0.31875 0.42396 0.31719 0.42396 0.30937 0.43548 0.30781 0.43548 0.30625 0.43779 0.30469 0.43779 0.30312 0.44009 0.29687 0.44009 0.29531 0.44239 0.29219 0.44239 0.29062 0.44009 0.27656 0.44009 0.27344 0.43548 0.27187 0.43548 0.26719 0.42857 0.26719 0.42627 0.26562 0.42396 0.26406 0.42396 0.2625 0.42166 0.2625 0.41935 0.26094 0.41935 0.25937 0.41705

第一行定义了狗的归一化多边形(class_id = 1),第二行定义了cat的归一化多边形(class_id = 0)。

添加和注释所有图像后,数据集就准备就绪。您需要创建两个数据集并将它们放在不同的文件夹中。最终文件夹结构看起来像这样:

Image description

在这里,位于“火车”文件夹中的训练数据集和位于“ Val”文件夹中的验证数据集。

最后,您需要创建一个数据集描述符YAML文件,该数据集指出创建数据集并描述其中的对象类。这是以上创建的数据的此文件的示例:

train: ../train/images
val: ../val/images

nc: 2
names: ['cat','dog']

在前两行中,您需要指定训练图像和验证数据集的路径。路径可以相对于当前文件夹或绝对。然后,nc行指定了这些数据集中存在的 n umber, c subses和names是按正确顺序的类名称。这些项目的索引是您在注释图像时使用的数字,当使用predict方法检测对象时,模型将返回这些索引。因此,如果您将“ 0”用于猫,那么它应该是names阵列中的第一项。

此YAML文件应传递到模型的train方法以开始培训过程。

为了使此过程更容易,存在很多程序可以在视觉上注释机器学习的图像。您可以向搜索引擎询问诸如“用于注释机器学习的软件”之类的内容,以获取它们的列表。还有许多在线工具可以完成所有这些工作。为此,很棒的在线工具之一是Roboflow Annotate。使用此服务,您只需要上传图像,在它们上绘制多边形,并为每个多边形设置类。然后,该工具将自动创建注释文件,将数据拆分进行训练和验证数据集,创建YAML描述符文件,然后您可以将带注释的数据导出并下载为zip文件。

在下一个视频中,我展示了如何使用roboflow创建“猫与狗”微数据集。

此示例只有两个图像,但是对于现实生活中的问题,该数据库应该更大。要训​​练一个好的模型,您应该拥有数百或数千个带注释的图像。

另外,准备图像数据库时,请尝试使其平衡。它应该具有每个类的相等数量的对象,例如狗和猫的数量相等。否则,对其进行训练的模型可以比另一个类更好。

数据准备就绪后,使用Python代码将其复制到文件夹中。

图像分割的训练过程与对象检测完全相同。您可以在本系列第一篇文章的appropriate section中阅读有关此内容的信息。

best.pt文件中训练了模型后,您可以使用它来预测特定对象类的细分掩码和多边形。

在生产中使用Yolov8分割模型

对于所有工作,我们使用了超级API,默认情况下为Yolov8包提供。这些API基于Pytorch框架,该框架用于创建当今神经网络的较大部分。一方面这很方便,但是对这些高级API的依赖也具有负面影响。如果您需要在生产中运行以这种方式创建的应用程序,则应在此处安装所有这些环境,包括Python,Pytorch和许多其他依赖关系。要在干净的新服务器上运行此操作,您需要下载并安装超过1 GB的第三方库!这绝对不是一个路。另外,如果您在生产环境中没有Python怎么办?如果您所有其他编写其他编程语言编写的其他代码,而您不打算使用Python怎么办?或者,如果您想在Android或iOS上的手机上运行模型怎么办?

使用Ultrytics软件包非常适合实验,训练和准备生产模型。但是,在生产本身中,您应该摆脱这些高级API。您必须直接加载并使用模型。为此,您需要了解Yolov8神经网络在引擎盖下的工作原理,并编写更多代码以提供模型的输入并从中处理输出。作为奖励,您将有机会使您的应用程序微小和快速,您无需安装Pytorch即可运行它们。

将Yolov8分割模型导出到ONNX

运行以下代码将Yolov8分割模型导出到ONNX:

model = YOLO("yolov8m-seg.pt")
model.export(format="onnx")

此代码应创建yolov8m-seg.onnx文件,该文件是中型Yolov8分割模型的ONNX版本。让我们来发现如何使用ONNX API而不是超级图。

进行预测。

使用ONNX加载​​模型

现在,当您拥有模型时,让我们使用ONNX来使用它。通过在Jupyter笔记本中运行以下命令来安装Python的ONNX运行时库:

!pip install onnxruntime

并导入它:

import onnxruntime as ort

我们将ort别名设置为它。 ort模块是ONNX API的根。该API的主要对象是InferenceSession,它用于实例化模型在其上进行预测。模型实例化的作用与我们以前用超级分析所做的非常相似:

model = ort.InferenceSession("yolov8m-seg.onnx")

在这里,我们加载了模型,但从“ .onnx”文件而不是“ .pt”上加载。现在可以运行了。

在继续阅读之前,请记住本文的appropriate section,我们在其中使用Yolov8 ONNX模型进行对象检测,因为图像分割模型也确实确实是对象检测,并且许多事物相似,并且此处将省略一些详尽的描述。我们将通过与那里相同的步骤:prepare the inputrun the modelprocess the output最终获得所有检测到的对象的分割蒙版和边界多边形。

准备输入

让我们看看,该模型期望接收并产生哪些输出的输入。

inputs = model.get_inputs()
len(inputs)
1

此代码表明该模型希望获得一个输入。现在,让我们显示有关此输入的信息:

input = inputs[0]

print("Name: ",input.name)
print("Shape: ",input.shape)
print("Type: ",input.type)
Name:  images
Shape:  [1, 3, 640, 640]
Type:  tensor(float)
如您所见,输入与对象检测模型相同。这是640x640像素的3通道图像。让我们以相同的方式准备它,使用Pillow模块加载图像:

from PIL import Image
import numpy as np

img = Image.open("cat_dog.jpg")

# save original image size for future
img_width, img_height = img.size;
# convert image to RGB,
img = img.convert("RGB");
# resize to 640x640
img = img.resize((640,640))

# convert the image to tensor 
# of [1,3,640,640] as required for 
# the model input
input = np.array(img)
input = input.transpose(2,0,1)
input = input.reshape(1,3,640,640).astype('float32')
input = input/255.0

因此,我们有可以通过模型传递的输入。

运行模型

让我们发现,输出模型将在运行之前会产生的哪个:

outputs = model.get_outputs()
len(outputs)
2

在这里您可以看到不同的东西。该模型会产生两个输出,而不是一个输出,因为它是用于对象检测的。让我们打印有关每个输出的信息:

for output in outputs:
    print("Name: ", output.name)
    print("Shape: ", output.shape)
    print("Type: ", output.type)
Name:  output0
Shape:  [1, 116, 8400]
Type:  tensor(float)
---
Name:  output1
Shape:  [1, 32, 160, 160]
Type:  tensor(float)
---

如前所述,分割模型输出对象检测边界框和分割蒙版。

  • output0-包含检测到的边界框和对象类,与对象检测相同
  • output1-包含用于检测到的对象的分割蒙版。只有原始面具,没有多边形。

让我们运行模型以接收这些输出:

outputs = model.run(None, {"images":input})
len(outputs)
2

现在您有了这两个输出,是时候处理它们了。

处理输出

模型返回了2个输出。让我们定义它们以方便并显示它们的形状:

output0 = outputs[0]
output1 = outputs[1]
print("Output0:",output0.shape,"Output1:",output1.shape)
Output0: (1, 116, 8400) Output1: (1, 32, 160, 160)

output0张量接近相同,我们在previous article中获得的对象检测。

output0 = output0[0].transpose()
output1 = output1[0]
print("Output0:",output0.shape,"Output1:",output1.shape)
Output0: (8400, 116) Output1: (32, 160, 160)

output0包含8400个检测到的对象(其中大多数是垃圾),但是,每个对象都有116个参数而不是84。这是因为它包含用于分割掩码的其他32个参数。该输出包括对象检测模型的相同84个参数和32个分割掩码。这就是为什么您需要将此输入分为两个部分:

boxes = output0[:,0:84]
masks = output0[:,84:]
print("Boxes:",boxes.shape,"Masks:",masks.shape)
Boxes: (8400, 84) Masks: (8400, 32)

加入边界框和口罩

boxes矩阵您可以处理与对象检测相同的方式。

masks矩阵中的数据不足以产生分割掩码。您需要以某种方式加入第二个输出。让我们打印masksoutput1形状彼此接近以查看如何加入它们:

print(masks.shape,output1.shape)
(8400, 32) (32, 160, 160)

您是否看到第一矩阵的列数与第二个行中的行数相同?这意味着您可以将这些矩阵乘以将它们连接在一起。加入它们后,您可能会收到(8400,160,160)。这些是所有检测到的盒子的分割面具。每个分割蒙版的大小为160x160。

但是,要使矩阵乘法您需要重塑output1以具有相同数量的尺寸:

output1 = output1.reshape(32,160*160)
print(masks.shape,output1.shape)
(8400, 32) (32, 25600)

后来您需要将25600重塑至160,160。

现在很明显您可以进行矩阵乘法。 numpy库的@运算符:

masks = masks @ output1
print(masks.shape)
(8400, 25600)

所以,现在您有8400个检测到的盒子和细分面具:

print(boxes.shape,masks.shape)
(8400, 84) (8400, 25600)

现在我们将它们连接在一起。让我们添加从第二矩阵到第一个矩阵的25600列:

boxes = np.hstack((boxes,masks))
print(boxes.shape)
(8400, 25684)

hstack函数通过从第二个数组到第一个数组的右侧的列将两个2D Numpy阵列水平连接。最后,对于每个检测到的对象,我们有以下列:

  • 0-4 -x_centery_centerwidthheight的边界盒
  • 4-84-所有80个类的对象类概率,该Yolov8模型可以检测到
  • 84-25684-分割蒙版的像素作为单行。实际上,分割蒙版是160x160矩阵,但我们只是将其弄平。

解析组合输出

现在没有什么可以阻止您解析此矩阵。对于除分段蒙版以外的所有内容,您可以重复使用the appropriate section of previous article中的相同代码。让我们从那里复制所需的功能和定义:

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"
]

def intersection(box1,box2):
    box1_x1,box1_y1,box1_x2,box1_y2 = box1[:4]
    box2_x1,box2_y1,box2_x2,box2_y2 = box2[:4]
    x1 = max(box1_x1,box2_x1)
    y1 = max(box1_y1,box2_y1)
    x2 = min(box1_x2,box2_x2)
    y2 = min(box1_y2,box2_y2)
    return (x2-x1)*(y2-y1)

def union(box1,box2):
    box1_x1,box1_y1,box1_x2,box1_y2 = box1[:4]
    box2_x1,box2_y1,box2_x2,box2_y2 = box2[:4]
    box1_area = (box1_x2-box1_x1)*(box1_y2-box1_y1)
    box2_area = (box2_x2-box2_x1)*(box2_y2-box2_y1)
    return box1_area + box2_area - intersection(box1,box2)

def iou(box1,box2):
    return intersection(box1,box2)/union(box1,box2)

现在让我们穿越检测到的对象的数组,解析并过滤它:

# stub of mask parsing function
def get_mask(row,box):
    mask = row.reshape(160,160)
    return mask

# parse and filter detected objects
objects = []
for row in boxes:
    prob = row[4:84].max()
    if prob < 0.5:
        continue
    xc,yc,w,h = row[:4]
    class_id = row[4:84].argmax()
    x1 = (xc-w/2)/640*img_width
    y1 = (yc-h/2)/640*img_height
    x2 = (xc+w/2)/640*img_width
    y2 = (yc+h/2)/640*img_height
    label = yolo_classes[class_id]
    mask = get_mask(row[84:25684],(x1,y1,x2,y2))
    objects.append([x1,y1,x2,y2,label,prob,mask])

# apply non-maximum suppression to filter duplicated
# boxes
objects.sort(key=lambda x: x[5], reverse=True)
result = []
while len(objects)>0:
    result.append(objects[0])
    objects = [object for object in objects if iou(object,objects[0])<0.7]

print(len(result))
2

如果您阅读了该文章,那么除get_mask以外的所有内容都应在此处清楚。最后,它返回2个检测到的对象,因为这应该是为了我们的图像。

过程分割面具

在当前阶段,get_mask功能只是将蒙版重新调整为160x160像素矩阵,但这不是完整的蒙版解析代码。要在此功能中编写正确的代码,让我们从结果中获取一些掩码并探索它是什么:

mask = result[0][6]
print(mask)
[[    -1.6534     -3.0664     -3.1947 ...     -7.4953     -6.7599     -5.0995]
 [     -2.258     -4.9611     -5.2562 ...     -9.0785     -8.8663     -7.7189]
 [    -2.6319     -5.4276     -5.5777 ...     -8.7668     -8.7555     -8.0009]
 ...
 [    -3.0467     -4.2886     -4.1702 ...     -5.1899     -5.0957     -4.4152]
 [    -3.0335     -4.4031     -4.4115 ...     -5.3815     -5.2315     -4.4424]
 [    -2.7506     -4.1725     -4.3196 ...      -4.692     -4.4167     -3.5068]]

此代码打印了第一个检测到的对象的掩码。每个像素的值是一个怪异的数字。实际上,此数字中的每个像素都属于对象。如果概率较低,则此像素为背景,并且该像素应为黑色,否则应为白色。但这是来自神经网络的原始输出,这些值应转换为概率。我们将使用sigmoid函数:

Image description

对于任何输入参数z,此函数只能在0到1的范围内返回值。这就是您可以在python上定义它的方式:

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

并将其应用于mask

mask = sigmoid(mask)
print(mask)
[[    0.16065    0.044514    0.039364 ...  0.00055536    0.001158   0.0060625]
 [    0.09466   0.0069568   0.0051882 ...  0.00011408  0.00014104  0.00044417]
 [   0.067116   0.0043745   0.0037671 ...   0.0001558  0.00015757  0.00033503]
 ...
 [   0.045361    0.013539    0.015214 ...   0.0055419   0.0060856    0.011948]
 [   0.045937    0.012091    0.011991 ...   0.0045797   0.0053169    0.011631]
 [   0.060052     0.01518     0.01313 ...   0.0090851     0.01193     0.02912]]

现在您有每个像素的概率,现在该将概率转换为真实颜色了。假设,如果概率较小或等于0.5,则将是黑色(颜色0),如果概率更大,则将是白色(颜色255)。

mask = (mask > 0.5).astype('uint8')*255
  • (mask > 0.5)创建了一个新的布尔矩阵,其大小相同,其中所有大于0.5的项目均为True,所有元素低于0.5,均为False
  • .astype('uint8')将这些值转换为整数:false至0,true为1
  • *255用于将所有像素的白色设置为255。

现在,这是一个真实的160x160图像阵列,其中所有属于对象的像素都是白色的,并且所有属于背景的像素都是黑色。您可以从此数组中创建枕头图像并显示:

img_mask = Image.fromarray(mask,"L")
img_mask

Image description

但是,它显示了整个图像的分割掩码,但是我们只需要第一个检测到的对象的掩码。让我们裁剪。

首先获取对象边界框的坐标:

x1,y1,x2,y2 = result[0][:4]

然后,请记住,蒙版的大小为160x160,但是针对真实图像大小计算的边界框的坐标,因此您需要将其缩放到蒙版的坐标:

mask_x1 = round(x1/img_width*160)
mask_y1 = round(y1/img_height*160)
mask_x2 = round(x2/img_width*160)
mask_y2 = round(y2/img_height*160)

然后使用它们裁剪口罩,再次转换为图像并显示:

mask = mask[mask_y1:mask_y2,mask_x1:mask_x2]

img_mask = Image.fromarray(mask,"L")
img_mask

Image description

最后一步是将此掩码扩展到该对象的边界框的大小。

img_mask = img_mask.resize((round(x2-x1),round(y2-y1)))
img_mask

Image description

并将此最终图像转换回蒙版数组:

mask = np.array(img_mask)

现在,您可以将所有蒙版处理代码复制到get_mask函数:

def get_mask(row,box):    
    mask = row.reshape(160,160)
    mask = sigmoid(mask)
    mask = (mask > 0.5).astype('uint8')*255
    x1,y1,x2,y2 = box
    mask_x1 = round(x1/img_width*160)
    mask_y1 = round(y1/img_height*160)
    mask_x2 = round(x2/img_width*160)
    mask_y2 = round(y2/img_height*160)
    mask = mask[mask_y1:mask_y2,mask_x1:mask_x2]
    img_mask = Image.fromarray(mask,"L")
    img_mask = img_mask.resize((round(x2-x1),round(y2-y1)))
    mask = np.array(img_mask)
    return mask

并重新启动Yolov8模型输出解析过程,以获取所有检测到的对象的正确掩码。

它几乎完成了,但是如您所记得的那样,Ultrytics API计算这些面具的多边形,我们将做同样的事情。

计算边界多边形

分割掩码是二进制图像。有不同的算法可以计算二进制图像的轮廓。如果只有两种颜色,那就不难。

这些算法之一是“通过边界对数字化二进制图像的拓扑结构分析”,该算法被广泛用于计算图像的矢量多边形。这是描述它的paper。但是不用担心,我们不会从头开始实施此功能。它已经针对大多数编程语言实现。特别是,您可以在findContours函数的OpenCV库中找到Python实现。确保已安装OPENCV:

!pip install opencv-python

然后,让我们定义一个函数,它将使用findContours函数从面具中获取边界多边形:

import cv2
def get_polygon(mask):
    contours = cv2.findContours(mask,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
    polygon = [[contour[0][0],contour[0][1]] for contour in contours[0][0]]
    return polygon

在第一行中,我们使用findContours函数获取掩模的所有轮廓,在第二行中,我们将此函数的输出(有点丑)转换为[x,y]坐标的数组。

现在,您可以将多边形添加到Yolov8输出解析循环中。这是一个完整的解析代码:

def get_mask(row,box):    
    mask = row.reshape(160,160)
    mask = sigmoid(mask)
    mask = (mask > 0.5).astype('uint8')*255
    x1,y1,x2,y2 = box
    mask_x1 = round(x1/img_width*160)
    mask_y1 = round(y1/img_height*160)
    mask_x2 = round(x2/img_width*160)
    mask_y2 = round(y2/img_height*160)
    mask = mask[mask_y1:mask_y2,mask_x1:mask_x2]
    img_mask = Image.fromarray(mask,"L")
    img_mask = img_mask.resize((round(x2-x1),round(y2-y1)))
    mask = np.array(img_mask)
    return mask

def get_polygon(mask):
    contours = cv2.findContours(mask,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
    polygon = [[contour[0][0],contour[0][1]] for contour in contours[0][0]]
    return polygon

objects = []
for row in boxes:
    prob = row[4:84].max()
    if prob < 0.5:
        continue
    xc,yc,w,h = row[:4]
    class_id = row[4:84].argmax()
    x1 = (xc-w/2)/640*img_width
    y1 = (yc-h/2)/640*img_height
    x2 = (xc+w/2)/640*img_width
    y2 = (yc+h/2)/640*img_height
    label = yolo_classes[class_id]
    mask = get_mask(row[84:25684],(x1,y1,x2,y2))
    polygon = get_polygon(mask)
    objects.append([x1,y1,x2,y2,label,prob,mask,polygon])

objects.sort(key=lambda x: x[5], reverse=True)
result = []
while len(objects)>0:
    result.append(objects[0])
    objects = [object for object in objects if iou(object,objects[0])<0.7]

在图像上绘制边界多边形

最后,为了演示整个过程的结果,我们将在图像上绘制检测到的对象的边界多边形。

img = Image.open("cat_dog.jpg")
draw = ImageDraw.Draw(img, "RGBA")

for object in result:
    [x1,y1,x2,y2,label,prob,mask,polygon] = object
    polygon = [(int(x1+point[0]),int(y1+point[1])) for point in polygon]
    draw.polygon(polygon,fill=(0,255,0,125))
img
  • 我们为它加载了图像和ImageDraw对象。
  • 然后我们通过检测到的对象循环
  • 假设左上角为(0,0)的多边形,但是我们需要从对象的左上角开始绘制它。这就是为什么我们通过将对象的左角(x1,y1)的坐标添加到每个点
  • 来转换每个对象的多边形。
  • 然后,我们在每个对象上绘制了一个填充的半透明多边形。

如果一切都很好,最终图像应该看起来像:

Image description

为方便起见,请看此视频。这是本章的整个编码会话:

创建一个分割Web应用程序

在Jupyter笔记本中进行研究很有趣,但是现在是时候进行真实实践了。我们将将上面用jupyter笔记本编写的代码集成到previous article中开发的对象检测Web应用程序。该应用程序不仅会检测到对象的边界框,还将检测它们的轮廓,并将它们绘制在图像顶​​部。

该应用程序将如下一个视频中所显示的外观和工作。

大多数代码将从上一个项目中重复使用,您可以在GitHub中找到。

是时候停止在Jupyter笔记本上工作并使用一些IDE与Python Web应用程序一起工作。

在这里,我显示了如何仅修改Python上创建的Web应用程序。但是,作为作业,使用相同的想法,您可以将实例分割为本文中提到的其他语言创建的项目,例如Julia,Node.js,JavaScript,Go and Rust,以及任何支持的其他编程语言ONNX运行时。

创建一个后端

我假设您将从object_detector.py文件重复使用后端。

需要实施以下更改:

导入OpenCV:

import cv2

然后,将我们使用的Yolov8分割模块复制到项目文件夹中,然后更改run_model函数以加载并返回2个输出:

def run_model(input):
    model = ort.InferenceSession("yolov8m-seg.onnx")
    outputs = model.run(None, {"images":input})
    return outputs

此功能运行模型并从中返回两个输出。

然后,更改process_output函数以接收这些输出并解析它们:

def process_output(outputs, img_width, img_height):
    output0 = outputs[0].astype("float")
    output1 = outputs[1].astype("float")
    output0 = output0[0].transpose()
    output1 = output1[0]
    boxes = output0[:, 0:84]
    masks = output0[:, 84:]
    output1 = output1.reshape(32, 160 * 160)
    masks = masks @ output1
    boxes = np.hstack((boxes, masks))

    objects = []
    for row in boxes:
        prob = row[4:84].max()
        if prob < 0.5:
            continue
        class_id = row[4:84].argmax()
        label = yolo_classes[class_id]
        xc, yc, w, h = row[:4]
        x1 = (xc - w/2) / 640 * img_width
        y1 = (yc - h/2) / 640 * img_height
        x2 = (xc + w/2) / 640 * img_width
        y2 = (yc + h/2) / 640 * img_height
        mask = get_mask(row[84:25684], (x1, y1, x2, y2), img_width, img_height)
        polygon = get_polygon(mask)
        objects.append([x1, y1, x2, y2, label, prob, polygon])

    objects.sort(key=lambda x: x[5], reverse=True)
    result = []
    while len(objects) > 0:
        result.append(objects[0])
        objects = [object for object in objects if iou(object, objects[0]) < 0.5]
    return result

此代码副本/从上一节中进行了较小的更改:get_mask功能也接受img_widthimg_height参数。

然后将其他助手功能复制到解析面具和多边形:

def get_mask(row,box, img_width, img_height):
    mask = row.reshape(160,160)
    mask = sigmoid(mask)
    mask = (mask > 0.5).astype('uint8')*255
    x1,y1,x2,y2 = box
    mask_x1 = round(x1/img_width*160)
    mask_y1 = round(y1/img_height*160)
    mask_x2 = round(x2/img_width*160)
    mask_y2 = round(y2/img_height*160)
    mask = mask[mask_y1:mask_y2,mask_x1:mask_x2]
    img_mask = Image.fromarray(mask,"L")
    img_mask = img_mask.resize((round(x2-x1),round(y2-y1)))
    mask = np.array(img_mask)
    return mask


def get_polygon(mask):
    contours = cv2.findContours(mask,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
    polygon = [[int(contour[0][0]), int(contour[0][1])] for contour in contours[0][0]]
    return polygon


def sigmoid(z):
    return 1 / (1 + np.exp(-z))

这就是后端应该更改的所有内容。 “/检测”端点返回一个检测到的对象数组。每个对象都包含polygon作为最后一项。

现在,让我们修改前端以在图像顶部绘制此多边形。

创建一个前端

在前端中,您只需要更改draw_image_and_boxes文件中的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,_,polygon]) => {
                  ctx.fillStyle = "rgba(0,255,0,0.5)";
                  ctx.beginPath();
                  polygon.forEach(([x,y]) => {
                    ctx.lineTo(x+x1,y+y1);
                  });
                  ctx.closePath();
                  ctx.fill();
                  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);
              });
          }
      }

请注意,在循环中,我们使用polygon变量在矩形和标签之前在图像顶部绘制填充的路径。我们使用半透明的绿色来填充此路径(rgba(0,255,0,0.5))。

仅此而已!现在您可以通过以下命令运行该应用:

python object_detector.py

在Web浏览器中打开http://localhost:8080,然后使用接口上传图像。如果所有代码都正确编写,您将不仅看到边界框,还会看到检测到的对象的分割掩码。

结论

在本文中,我解释了实例分割机器学习任务,并使用Yolov8神经网络模型展示了如何实施它。我们涵盖了高水平的超级API和低水平的ONNX API。最后,我们创建了一个Web应用程序,以检测图像上检测到的对象的边界多边形。

您可以在此存储库中找到此Web应用程序的源代码:

https://github.com/AndreyGermanov/yolov8_segmentation_python

此外,此存储库包含带有实例分割代码的jupyter笔记本,包括用于ulralytics和onnx。

如果您比较了超级API和ONNX API产生的口罩,则可能会发现ONNX示例返回质量较低的口罩。这是因为我们仅在将其传递给神经网络之前仅使用基本输入图像处理。 Ultrytics .predict功能实现了更多图像预处理步骤。作为另一个实践,您可以打开并学习source code of the "predict" function以理解,哪些过滤和转换适用于输入图像。然后,您可以尝试自行实施onnx推理并查看差异。

谢谢你,直到下一次!

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

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