65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Raspberry Pi 和 Arm NN 机器学习实现自动垃圾分类

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2019 年 12 月 17 日

CPOL
viewsIcon

28290

在本文中,我们将演示如何为物联网设备构建一个基于 Arm NN 的应用程序,该应用程序可以通过图像分析执行垃圾自动分类。

通过神经网络实现的机器学习 (ML) 正在为软件带来令人兴奋的新推理能力。通常,ML 模型在云端运行,这意味着要进行分类或预测,您需要通过网络将文本、声音或图像发送给外部供应商。此类解决方案的性能强烈依赖于网络带宽和延迟。此外,将数据发送到外部服务会引入潜在的隐私问题。

为了解决这些问题,人工智能现在正向边缘设备迁移。Arm 正通过提供加速硬件 IP 和中间层解决方案来引领这场革命。

Arm 的开源软件工具 Arm NN 帮助开发人员一次构建应用程序,然后无缝、轻松地将其移植到不同的平台。

Arm NN 和 Arm Compute Library 是用于优化基于 Arm 的处理器和物联网边缘设备上的 ML 的开源库。ML 工作负载可以完全在边缘设备上运行,使复杂的 AI 软件几乎可以在任何地方运行,即使没有网络访问。

实际上,这意味着您可以使用最流行的 ML 框架训练您的模型。然后,您可以在边缘设备上加载和优化模型以执行快速推理,从而消除网络和隐私问题。

为了演示边缘计算资源与 ML 的结合,我们将使用 Arm NN API 对连接到 Raspberry Pi 的网络摄像头拍摄的垃圾图像进行分类。Raspberry Pi 将显示分类结果。

本教程我们需要什么?

以下是我们为本教程创建设备和应用程序所使用的物品

  • 一个 Raspberry Pi 设备。本教程已在 Pi 2、Pi 3 和 Pi 4 Model B 上进行验证。
  • 一张 MicroSD 卡
  • 一个用于 Raspberry Pi 的 USB 或 MIPI 摄像头模块
  • 如果您想构建自己的 Arm NN 库,您还需要一台 Linux 主机或一台安装了 Linux 虚拟环境的计算机。
  • 玻璃、纸张、纸板、塑料、金属或任何您的 Raspberry Pi 可以帮助您分类的垃圾!

设备设置

我使用了 Raspberry Pi 4 Model B,它配备了四核 ARM Cortex A72 CPU、1GB 板载内存、8GB MicroSD 卡、一个 WiFi 加密狗、一个 USB 摄像头 (Microsoft HD-3000)。

在设备通电之前,您需要按照 设置 Raspberry Pi 教程 中的说明在 MicroSD 卡上安装 Raspbian。 我使用了 NOOBS 进行简易设置。

然后我启动了我的 Raspberry Pi,配置了 Raspbian,并设置了虚拟网络计算 (VNC) 以远程访问我的 Raspberry Pi。您可以在 RaspberryPi.org 的 VNC (Virtual Network Computing) 页面找到设置 VNC 的详细说明。

配置完硬件后,我开始处理软件。该解决方案由三个组件组成

  • camera.hpp 实现了从网络摄像头捕获图像的辅助方法。
  • ml.hpp 包含根据摄像头输入加载 ML 模型和分类垃圾图像的方法。
  • main.cpp 包含 main 方法,它将上述组件连接起来。这是应用程序的入口点。

所有这些组件都将在下面讨论。您在这里看到的一切都是使用 Geany 编辑器创建的,该编辑器随默认的 Raspbian 安装一起提供。

设置摄像头

为了从网络摄像头获取图像,我使用了 OpenCV,这是一个计算机视觉 (CV) 开源库。该库提供了一个方便的接口来捕获和处理图像。您可以轻松地在不同的应用程序和设备(从物联网到移动设备再到桌面)中使用相同的 API。

将 OpenCV 集成到您的物联网 Raspbian 应用程序中最简单的方法是通过 apt-get 安装 libopencv-dev 包

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install libopencv-dev

下载并安装软件包后,您就可以从网络摄像头捕获图像了。我首先实现了两个方法:grabFrameshowFrame(请参阅附带代码中的 camera.hpp

Mat grabFrame()
{
  // Open default camera
  VideoCapture cap(0);

  // If camera was open, get the frame
  Mat frame;
  if (cap.isOpened())
  {
cap >> frame;
    imwrite("image.jpg", frame); 
  }
  else
  {
    printf("No valid camera\n");
  }

  return frame;
}
 
void showFrame(Mat frame)
{
  if (!frame.empty())
  {
    imshow("Image", frame);
    waitKey(0);
  }
}

第一个方法 grabFrame 打开默认网络摄像头(索引为 0)并捕获单个帧。请注意,C++ OpenCV 接口使用 Mat 类表示图像,因此 grabFrame 返回此类型的对象。可以通过读取 Mat 类的 data 成员来访问原始图像数据。

第二个方法 showFrame 用于显示捕获的图像。为此,showFrame 使用 OpenCV 的 imshow 方法创建一个窗口,图像将在此窗口中显示。然后我调用 waitKey 方法,该方法强制显示图像窗口,直到用户按下键盘上的某个键。我使用 waitKey 参数来指定超时。在这里,我使用了无限超时,用 0 表示。

为了测试上述方法,我在 main 方法 (main.cpp) 中调用了它们

int main()
{
  // Grab image
  Mat frame = grabFrame();
 
  // Display image
  showFrame(frame);
 
  return 0;
}

为了构建应用程序,我使用了 g++ 命令并通过 pkg-config 链接了 OpenCV 库

g++ main.cpp -o trashClassifier 'pkg-config --cflags --libs opencv'

之后,我执行了应用程序以捕获单个图像

垃圾数据集和模型训练

我使用 Gary Thung 创建的现有数据集训练了 TensorFlow 分类模型,该数据集可从他的 trashnet Github 仓库 获取。

为了训练模型,我遵循了 TensorFlow 图像分类教程 的步骤。但是,我在 256x192 像素的图像上训练了模型——原始垃圾数据集图像宽度和高度的一半。这是我们的模型组成部分

# Building the model
model = Sequential()
# 3 convolutional layers
model.add(Conv2D(32, (3, 3), input_shape = (IMG_HEIGHT, IMG_WIDTH, 3)))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(64, (3, 3)))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(64, (3, 3)))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))

# 2 hidden layers
model.add(Flatten())
model.add(Dense(128))
model.add(Activation("relu"))

model.add(Dense(128))
model.add(Activation("relu"))

# The output layer with 6 neurons, for 6 classes
model.add(Dense(6))
model.add(Activation("softmax"))

该模型实现了大约 83% 的准确率。我们使用 tf.lite.TFLiteConverter 将其转换为 TensorFlow Lite 格式 trash_model.tflite

converter = tf.lite.TFLiteConverter.from_keras_model_file('model.h5') 
model = converter.convert()
file = open('model.tflite' , 'wb') 
file.write(model)

设置 Arm NN SDK

下一步是准备 Arm NN SDK。要为您的 Raspberry Pi 生成 Arm NN 库,您可以遵循 Arm 交叉编译 Arm NN 和 Tensorflow for the Raspberry Pi 教程,或者运行 Arm Tool-Solutions Github 仓库 中的自动化脚本来交叉编译 SDK。适用于 Raspberry Pi 的 Arm NN 19.08 二进制 tar 文件可以在 GitHub 上找到。

无论您选择哪种方法,都将生成的 tar (armnn-dist) 复制到您的 Raspberry Pi。在这种情况下,我使用 VNC 在我的开发 PC 和 Raspberry Pi 之间传输文件。

接下来,设置 LD_LIBRARY_PATH 环境变量以指向 armnn-distarmnn/lib 子文件夹

export LD_LIBRARY_PATH=/home/pi/armnn-dist/armnn/lib

这里,我假设 armnn-dist 位于 home/pi 文件夹下。

使用 Arm NN 进行设备端 ML 推理

加载模型输出标签

您必须使用模型输出标签来解释模型的输出。在我们的代码中,我们创建了一个字符串向量来存储 6 个类别。

const std::vector<std::string> modelOutputLabels = {"cardboard", "glass", "metal", "paper", "plastic", "trash"};

加载和预处理输入图像

在模型将图像用作输入之前,我们必须对其进行预处理。我们使用的预处理方法取决于框架、模型或模型数据类型。

我们的模型有一个转换 2D 层作为输入,ID 为“conv2d_input”。它的输出是一个 softmax 激活层,ID 为“activation_5/Softmax”。模型属性通过 Tensorboard(TensorFlow 提供的用于模型检查的可视化工具)提取。

const std::string inputName = "conv2d_input";
const std::string outputName = "activation_5/Softmax";

const unsigned int inputTensorWidth = 256;
const unsigned int inputTensorHeight = 192;
const unsigned int inputTensorBatchSize = 32;
const armnn::DataLayout inputTensorDataLayout = armnn::DataLayout::NHWC;

请注意,用于训练的批量大小等于 32,因此您需要提供至少 32 张图像用于训练和验证。

以下代码加载并预处理摄像头捕获的图像

// Load and preprocess input image
const std::vector<TContainer> inputDataContainers =
{ PrepareImageTensor<uint8_t>("image.jpg" ,
 inputTensorWidth, inputTensorHeight,
 normParams,
 inputTensorBatchSize,
 inputTensorDataLayout) } ;

所有与加载 ML 模型和进行预测相关的逻辑都包含在 ml.hpp 文件中,

创建解析器并加载网络

使用 Arm NN 的下一步是创建一个解析器对象,该对象将用于加载您的网络文件。Arm NN 具有各种模型文件类型的解析器,包括 TFLite、ONNX、Caffe 等。解析器处理底层 Arm NN 图的创建,因此您无需手动构建模型图。

在此示例中,我们使用 TFLite 模型,因此我们创建 TfLite 解析器以从指定路径加载模型。

ml.hpp 中最重要的方法是 loadModelAndPredict。它首先创建 TensorFlow 模型解析器

// Import the TensorFlow model. 
// Note: use CreateNetworkFromBinaryFile for .tflite files.
armnnTfLiteParser::ITfLiteParserPtr parser = 
  armnnTfLiteParser::ITfLiteParser::Create();
armnn::INetworkPtr network = 
  parser->CreateNetworkFromBinaryFile("trash_model.tflite");

接下来,我调用 armnnTfLiteParser::ITfLiteParser::Create 方法,并使用此解析器加载 trash_model.tflite。

解析模型后,您可以使用 GetNetworkInputBindingInfo / GetNetworkOutputBindingInfo 方法创建与这些层的绑定

// Find the binding points for the input and output nodes
const size_t subgraphId = 0;
armnnTfParser::BindingPointInfo inputBindingInfo = 
    parser->GetNetworkInputBindingInfo(subgraphId, inputName);
armnnTfParser::BindingPointInfo outputBindingInfo = 
    parser->GetNetworkOutputBindingInfo(subgraphId, outputName);

您必须准备一个容器来接收模型的输出。输出张量大小等于模型输出标签的数量。这实现如下

// Output tensor size is equal to the number of model output labels
const unsigned int outputNumElements = modelOutputLabels.size();
std::vector<TContainer> outputDataContainers = { std::vector<uint8_t>(outputNumElements)};

选择后端,创建运行时,并优化模型

您必须优化您的网络并将其加载到计算设备上。Arm NN SDK 支持在 Arm CPU、Mali GPU 和 DSP 设备上进行优化的执行后端。后端由一个字符串标识,该字符串在所有后端中必须是唯一的。您可以按偏好顺序指定一个或多个后端。

在我们的代码中,Arm NN 决定哪些层受后端支持。首先检查 CPU。如果一个或多个层无法在 CPU 上运行,它将首先回退到参考实现。

指定后端列表后,您可以在运行时创建运行时并优化网络。后端可以选择实现后端特定的优化。Arm NN 根据后端将图拆分为子图,它对每个子图调用优化子图函数,并在可能的情况下用其优化版本替换原始图中的相应子图。

完成此操作后,LoadNetwork 为层创建后端特定工作负载,它创建一个后端特定工作负载工厂并调用它来创建工作负载。输入图像包装在常量张量中并绑定到输入张量。

// Optimize the network for a specific runtime compute 
// device, e.g. CpuAcc, GpuAcc
armnn::IRuntime::CreationOptions options;
armnn::IRuntimePtr runtime = armnn::IRuntime::Create(options);
armnn::IOptimizedNetworkPtr optNet = armnn::Optimize(*network,
   {armnn::Compute::CpuAcc, armnn::Compute::CpuRef},  
   runtime->GetDeviceSpec());

Arm NN SDK 推理引擎在现有神经网络框架与 Arm Cortex-A CPU、Arm Mali GPU 和 DSP 之间架起了一座桥梁。使用 Arm NN SDK 运行 ML 推理可确保 ML 算法针对底层硬件进行优化。

优化后,您将网络加载到运行时

// Load the optimized network onto the runtime device
armnn::NetworkId networkIdentifier;
runtime->LoadNetwork(networkIdentifier, std::move(optNet));

然后,使用 EnqueueWorkload 方法执行预测

// Predict
armnn::Status ret = runtime->EnqueueWorkload(networkIdentifier,
      armnnUtils::MakeInputTensors(inputBindings, inputDataContainers),
      armnnUtils::MakeOutputTensors(outputBindings, outputDataContainers));

最后一步为您提供预测结果。

std::vector<uint8_t> output = boost::get<std::vector<uint8_t>>(outputDataContainers[0]);

size_t labelInd = std::distance(output.begin(), std::max_element(output.begin(), output.end()));
std::cout << "Prediction: ";
std::cout << modelOutputLabels[labelInd] << std::endl;

在上面的例子中,唯一与 ML 框架固有相关的方面是加载模型和配置绑定的部分。其他一切都与 ML 框架无关。因此,您可以轻松地在各种 ML 模型之间切换,而无需修改应用程序的其他部分。

整合并构建应用程序

最后,我将所有组件整合到 main 方法 (main.cpp) 中

#include "camera.hpp"
#include "ml.hpp"
 
int main()
{
  // Grab frame from the camera
  grabFrame(true);
 
  // Load ML model and predict
  loadModelAndPredict();
 
  return 0;
}

请注意,grabFrame 有一个额外的参数。当此参数为 true 时,摄像头图像将转换为灰度并调整大小为 256x192 像素,以匹配 ML 模型的输入格式,然后将转换后的图像传递给 loadModelAndPredict 方法。

要构建应用程序,请使用 g++ 命令并链接 OpenCV 和 Arm NN SDK

g++ main.cpp -o trashClassifier 'pkg-config --cflags --libs opencv' -I/home/pi/armnn-dist/armnn/include -I/home/pi/armnn-dist/boost/include -L/home/pi/armnn-dist/armnn/lib -larmnn -lpthread -linferenceTest -lboost_system -lboost_filesystem -lboost_program_options -larmnnTfLiteParser -lprotobuf

同样,我假设 Arm NN SDK 位于 home/pi/armnn-dist 文件夹中。启动应用程序并拍摄一些纸板的照片。

pi@raspberrypi:~/ $ ./trashClassifier
ArmNN v20190800

Running network...
Prediction: cardboard

如果在应用程序执行期间,您看到消息“error while loading shared libraries: libarmnn.so: cannot open shared object file: No such file or directory”,请检查以确保您的 LD_LIBRARY_PATH 设置正确。

总结

您还可以通过外部信号触发图像捕获和识别来改进应用程序。为此,您需要修改 ml.hpp 模块中的 loadAndPredict 方法。您需要将模型加载与预测(推理)分开。

本文展示了如何将 AI 从云端转移到边缘。Arm 在这一转变中处于领先地位,它拥有为推理量身定制的硬件和软件组件,无论您选择哪种 ML 框架:TensorFlow、TensorFlow Core、Caffe 或任何其他 ML 框架,都能帮助您实现解决方案。

您还可以参考 Arm NN 操作指南获取更多详细信息

关于 Arm

请访问 Arm 解决方案页面,了解更多我们正在 AI 和 ML 领域开展的工作。如果您是一家希望与 Arm 合作的企业,请查看我们的 Arm AI 合作伙伴计划

© . All rights reserved.