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





0/5 (0投票)
在本文中,我们将演示如何为物联网设备构建一个基于 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
下载并安装软件包后,您就可以从网络摄像头捕获图像了。我首先实现了两个方法:grabFrame
和 showFrame
(请参阅附带代码中的 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-dist
的 armnn/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 合作伙伴计划。