这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到毕业答辩的要求,这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。
为了大家能够顺利以及最少的精力通过毕设,学长分享优质毕业设计项目,今天要分享的是
基于树莓派的智能捡垃圾机器人
学长这里给一个题目综合评分(每项满分5分)
选题指导, 项目分享:
https://gitee.com/dancheng-senior/project-sharing-1/blob/master/%E6%AF%95%E8%AE%BE%E6%8C%87%E5%AF%BC/README.md
通过这款自动驾驶机器人,可进行物体检测识别和监控垃圾。它还部署了视频流和跌倒检测系统。
使用 Edge Impulse 构建了一个神经网络模型,用于在这三类下通过物体检测来检测垃圾:
Edge Impulse 还可以在连接到 Raspberry Pi 后提供实时视频流。因此,无需创建网络摄像头服务器 (Motion) 即可使用 Raspberry Pi 为该项目进行直播。
Edge Impulse是一个免费的嵌入式机器学习开发平台,供开发人员(新手或专家)从学习到部署。它具有许多功能和内置神经网络模型,可满足各种要求,例如用于多目标检测的迁移学习。此外,该平台还提供来自连接设备摄像头的实时视频流。因此,本项目使用 Edge Impulse 来识别和监控垃圾。
在构建用于物体检测的神经网络模型之前,需要创建一个平衡良好的数据集来检测多个垃圾类别:
瓶子(玻璃和塑料)
罐(金属)
包装(塑料、纸、纸板等)
通过精心挑选与上述垃圾类别相关的最合适的图像,结合了废物和垃圾的两个不同数据集:
垃圾分类数据集
TACO 垃圾数据集
选择后,每个类别大约有100张图像,总共292张。通常,像这样的小数据集无法在垃圾检测中得到准确的结果。然而,Edge Impulse 在训练模型时采用了迁移学习,所以得到了相当不错的结果,而且准确率很高。
首先,注册Edge Impulse并创建一个新项目(垃圾检测机器人)。
为了能够使用对象检测模型,请转到仪表板 ➡ 项目信息 ➡ 标签方法并选择Bounding box (object detection) 。
然后,转到数据获取并选择上传数据(上传现有数据)。
成功上传垃圾数据集后,用提到的三个垃圾类别标记每个图像 -瓶子、罐头、包装。在 Edge Impulse 中,标记一个对象,类似在它周围拖动一个框并输入一个标签一样简单。此外,Edge Impulse 在标记对象时在后台运行跟踪算法,因此它会自动为不同图像中的相同对象移动框。
⭐转至数据获取➡标签队列(Object detection labeling)。它显示了数据集中剩余的所有未标记图像。
⭐然后,选择一个未标记的图像,拖动框,单击Save labels ,然后重复此操作,直到整个数据集都被标记。
完成标记后,可以看到在数据采集下列出了一个平衡良好的数据集,用于垃圾检测。
脉冲是边缘脉冲中的自定义神经网络模型。在这个项目中,设计了一个脉冲,它获取原始图像数据,调整图像大小,使用预处理块来处理图像,然后利用学习块对新数据进行分类:
图像预处理块 ➡ 取彩色图像中的数据,可选地使图像灰度化,然后将数据转化为特征数组。
迁移学习学习块 -对象检测(图像)➡ 接收所有图像并学习区分三种(瓶子、罐子、包装)垃圾类别。
预处理块总是为相同的输入返回相同的值(例如,将彩色图像转换为灰度图像),而学习块则从过去的经验中学习。除了内置的预处理块,Edge Impulse 还允许用户创建自定义预处理块(步骤)。
⭐进入创建脉冲,设置图像宽度和图像高度为320,调整大小模式为适合最短轴。然后,添加图像和对象检测(图像)块。最后,单击Save Impulse 。
配置处理块和功能
⭐ 要配置处理块,请转到Impulse design下的Image ,选择颜色深度为RGB ,然后单击Save parameters 。处理块为模型适当地格式化原始图像数据。
然后,在特征生成屏幕上,单击生成特征以:
它正在努力从头开始构建准确的计算机视觉模型,因为该模型需要各种各样的输入数据才能很好地泛化,并且在 GPU 上训练此类模型可能需要数天时间。然而,Edge Impulse 在训练用于对象检测的神经网络模型时采用了迁移学习。转移学习方法重新训练训练有素的神经网络模型的上层以进行对象检测,从而产生更可靠的模型,这些模型可以在很短的时间内进行训练并使用更小的数据集。
尽管迁移学习使训练对象检测模型变得轻松,但使用机器学习识别和监控垃圾仍然具有挑战性。由于垃圾种类的颜色、形状和材料各不相同,因此我从两个不同的数据集中精心挑选了 292 张最合适的图像,如前面的步骤所述。处理完我的数据集后,我用整个数据集训练模型以区分三种不同的垃圾类别(瓶子、罐头、包装)。
使用数据集训练模型后,Edge Impulse 将精度分数(准确度)评估为60.2% 。
⭐ 进入Impulse design下的Object detection ,选择默认的基础模型,并设置:
训练周期数➡ 40
学习率➡ 0.01
验证神经网络模型
由于使用整个数据集来训练模型,因此上传了新图像作为测试数据集来验证模型。
在验证模型后,推断它区分瓶子(塑料和玻璃)和罐子(金属)的准确率超过 88%。然而,它很难检测包装(塑料、纸、纸板等),因为包装在很多方面都不同——形状、颜色、材料等。由于收集的种类繁多,包装的准确度在 45% 到 55% 之间。因此,我仍在收集数据以改进我的数据集和包装的准确性。
使用测试数据集(大约 50 张图像),Edge Impulse 评估模型精度为84.11% 。
⭐ 要验证模型,请转到模型测试并选择Classify all 。
经过设计、训练、验证和验证后,剩下的就是将模型部署到了树莓派 4。由于 Edge Impulse 官方支持树莓派 4,因此使用此开发板部署和运行模型非常简单。
⭐首先,打开终端,运行以下命令安装依赖和模块:
安装依赖项后,将USB网络摄像头连接到Raspberry Pi 4并运行以下命令:边缘脉冲Linux
然后,登录并使用终端向导选择一个Edge Impulse项目(垃圾检测机器人)。
要验证树莓派 4 是否成功连接到所选 Edge Impulse 项目,请转到项目页面并单击Devices 。
⭐ 要在 Raspberry Pi 4 本地部署和运行模型,请打开终端并在下面输入以下命令:
edge-impulse-linux-runner
⭐ 然后,Edge Impulse 会自动编译具有完整硬件加速的模型并将其下载到 Raspberry Pi 4。在这方面,该模型以最小的延迟和功耗运行。
在 Raspberry Pi 4 上部署模型后,将它和 USB 网络摄像头连接到机器人底盘。就可运行模型,就可以在三个垃圾类别之间进行分类。
由于 Edge Impulse 在模型运行时提供带有来自连接的网络摄像头的分类结果的实时视频流,因此当垃圾检测机器人运行时,不需要创建带有 Motion 或其他模块的网络摄像头服务器来显示分类结果。
⭐ 要显示实时视频流和分类结果,直接在运行模型后转到终端中给定的 URL:
为了让垃圾检测机器人自主移动,使用了 RPLIDAR A1M8-R6 - 360 度激光扫描仪(激光雷达)。该激光雷达可以在 12 米范围内执行 360 度扫描,每秒生成多达 8000 个样本。
没有绘制环境来导航机器人(SLAM),而是使用激光雷达来检测三个不同方向(右、左和前)的障碍物,因为我想让机器人在不受环境(室内或室外)任何限制的情况下运行)。
为了获得 RPLIDAR A1M8 和 Raspberry Pi 生成的 360 度扫描数据,我使用了Adafruit CircuitPython RPLIDAR库。
在使用库收集数据之前,将 RPLIDAR A1M8 连接到计算机 (Windows) 并运行Frame Grabber应用程序以在扫描周围环境的同时检查角度方向和距离测量值。
RPLIDAR A1M8 不是采用步进读取方法,而是通过直流电机驱动旋转扫描仪,并根据扫描仪获得的角度生成距离读数。这样,单次旋转不能保证为每个可能的角度(从 0 到 360)产生距离值。只需旋转几次,即可使用此激光雷达进行完整的 360 度扫描。因此,在不调试生成的扫描数据点的情况下,使用 RPLIDAR A1M8 进行避障可能会非常棘手和困难。
在设置好RPLIDAR A1M8并包括所需的模块后,对扫描仪每转产生的不完整的360度扫描数据进行调试和处理,以检测三个不同方向的障碍物。对于每个方向,定义了一个起始角和一个终止角(顺时针):
右 ➡ 开始:60 ,结束:120
左 ➡ 开始:240 ,结束:300
前 ➡ 开始:340结束:20
在每个方向范围内,代码搜索准确的距离测量作为起点和终点:
起点 ➡ 从起始角到起始角 + 15 ,
终点 ➡ 从终点角度到终点角度 - 15 .
这样,代码覆盖每个方向的 30 度范围,以得出准确的距离测量值(起点和终点),不会出现错误或遗漏。然后,如果引出的方向起点和终点小于给定阈值(40 cm),则代码激活机器人底盘(L298N)以避开该方向检测到的障碍物。
在完成代码并将激光雷达(RPLIDAR A1M8)安装到机器人底盘上后,在外壳上测试了垃圾检测机器人的避障系统。
完成上述步骤后,为垃圾检测机器人添加一个 6 轴加速度计作为跌倒检测系统,以防止碰撞。跌倒检测系统还显示 X、Y 和 Z 轴的加速度测量值。
下载所需的库以从 DFRobot 串行 6 轴加速度计获取数据。
下载控制 SSD1306 OLED 屏幕所需的库。
为了构建机器人的跌倒检测系统,我将 DFRobot 串行 6 轴加速度计、SSD1306 OLED 屏幕(128x32)和蜂鸣器连接到 Arduino Nano。为了提供 Arduino Nano,将它连接到 Raspberry Pi 4:
/*!
@file getLightIntensity.ino
@Set the frequency of data output by the sensor, read the acceleration, angular velocity, and angle of X, Y, and Z axes.
@n Experimental phenomenon: when the sensor starts, it outputs data at the set frequency and the data will be displayed on serial monitor
@copyright Copyright (c) 2010 DFRobot Co.Ltd (http://www.dfrobot.com)
@licence The MIT License (MIT)
@author [huyujie]([email protected])
@version V1.0
@date 2020-12-03
@https://github.com/DFRobot
*/
#include
#include
//Use software serial port RX:10,TX:11
SoftwareSerial mySerial(10, 11);
DFRobot_WT61PC sensor(&mySerial);
void setup()
{
//Use Serial as debugging serial port
Serial.begin(115200);
//Use software serial port mySerial as communication seiral port
mySerial.begin(9600);
//Revise the data output frequncy of sensor FREQUENCY_0_1HZ for 0.1Hz, FREQUENCY_0_5HZ for 0.5Hz, FREQUENCY_1HZ for 1Hz, FREQUENCY_2HZ for 2Hz,
// FREQUENCY_5HZ for 5Hz, FREQUENCY_10HZ for 10Hz, FREQUENCY_20HZ for 20Hz, FREQUENCY_50HZ for 50Hz,
// FREQUENCY_100HZ for 100Hz, FREQUENCY_125HZ for 125Hz, FREQUENCY_200HZ for 200Hz.
sensor.modifyFrequency(FREQUENCY_10HZ);
}
void loop()
{
if (sensor.available()) {
Serial.print("Acc\t"); Serial.print(sensor.Acc.X); Serial.print("\t"); Serial.print(sensor.Acc.Y); Serial.print("\t"); Serial.println(sensor.Acc.Z); //acceleration information of X,Y,Z
Serial.print("Gyro\t"); Serial.print(sensor.Gyro.X); Serial.print("\t"); Serial.print(sensor.Gyro.Y); Serial.print("\t"); Serial.println(sensor.Gyro.Z); //angular velocity information of X,Y,Z
Serial.print("Angle\t"); Serial.print(sensor.Angle.X); Serial.print("\t"); Serial.print(sensor.Angle.Y); Serial.print("\t"); Serial.println(sensor.Angle.Z); //angle information of X, Y, Z
Serial.println(" ");
}
}
void Adafruit_SSD1306::drawPixel(int16_t x, int16_t y, uint16_t color) {
if ((x >= 0) && (x < width()) && (y >= 0) && (y < height())) {
// Pixel is in-bounds. Rotate coordinates if needed.
switch (getRotation()) {
case 1:
ssd1306_swap(x, y);
x = WIDTH - x - 1;
break;
case 2:
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
break;
case 3:
ssd1306_swap(x, y);
y = HEIGHT - y - 1;
break;
}
switch (color) {
case SSD1306_WHITE:
buffer[x + (y / 8) * WIDTH] |= (1 << (y & 7));
break;
case SSD1306_BLACK:
buffer[x + (y / 8) * WIDTH] &= ~(1 << (y & 7));
break;
case SSD1306_INVERSE:
buffer[x + (y / 8) * WIDTH] ^= (1 << (y & 7));
break;
}
}
}
/*!
@brief Clear contents of display buffer (set all pixels to off).
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void Adafruit_SSD1306::clearDisplay(void) {
memset(buffer, 0, WIDTH * ((HEIGHT + 7) / 8));
}
/*!
@brief Draw a horizontal line. This is also invoked by the Adafruit_GFX
library in generating many higher-level graphics primitives.
@param x
Leftmost column -- 0 at left to (screen width - 1) at right.
@param y
Row of display -- 0 at top to (screen height -1) at bottom.
@param w
Width of line, in pixels.
@param color
Line color, one of: SSD1306_BLACK, SSD1306_WHITE or SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void Adafruit_SSD1306::drawFastHLine(int16_t x, int16_t y, int16_t w,
uint16_t color) {
bool bSwap = false;
switch (rotation) {
case 1:
// 90 degree rotation, swap x & y for rotation, then invert x
bSwap = true;
ssd1306_swap(x, y);
x = WIDTH - x - 1;
break;
case 2:
// 180 degree rotation, invert x and y, then shift y around for height.
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
x -= (w - 1);
break;
case 3:
// 270 degree rotation, swap x & y for rotation,
// then invert y and adjust y for w (not to become h)
bSwap = true;
ssd1306_swap(x, y);
y = HEIGHT - y - 1;
y -= (w - 1);
break;
}
if (bSwap)
drawFastVLineInternal(x, y, w, color);
else
drawFastHLineInternal(x, y, w, color);
}
/*!
@brief Draw a horizontal line with a width and color. Used by public
methods drawFastHLine,drawFastVLine
@param x
Leftmost column -- 0 at left to (screen width - 1) at right.
@param y
Row of display -- 0 at top to (screen height -1) at bottom.
@param w
Width of line, in pixels.
@param color
Line color, one of: SSD1306_BLACK, SSD1306_WHITE or
SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void Adafruit_SSD1306::drawFastHLineInternal(int16_t x, int16_t y, int16_t w,
uint16_t color) {
if ((y >= 0) && (y < HEIGHT)) { // Y coord in bounds?
if (x < 0) { // Clip left
w += x;
x = 0;
}
if ((x + w) > WIDTH) { // Clip right
w = (WIDTH - x);
}
if (w > 0) { // Proceed only if width is positive
uint8_t *pBuf = &buffer[(y / 8) * WIDTH + x], mask = 1 << (y & 7);
switch (color) {
case SSD1306_WHITE:
while (w--) {
*pBuf++ |= mask;
};
break;
case SSD1306_BLACK:
mask = ~mask;
while (w--) {
*pBuf++ &= mask;
};
break;
case SSD1306_INVERSE:
while (w--) {
*pBuf++ ^= mask;
};
break;
}
}
}
}
/*!
@brief Draw a vertical line. This is also invoked by the Adafruit_GFX
library in generating many higher-level graphics primitives.
@param x
Column of display -- 0 at left to (screen width -1) at right.
@param y
Topmost row -- 0 at top to (screen height - 1) at bottom.
@param h
Height of line, in pixels.
@param color
Line color, one of: SSD1306_BLACK, SSD1306_WHITE or SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void Adafruit_SSD1306::drawFastVLine(int16_t x, int16_t y, int16_t h,
uint16_t color) {
bool bSwap = false;
switch (rotation) {
case 1:
// 90 degree rotation, swap x & y for rotation,
// then invert x and adjust x for h (now to become w)
bSwap = true;
ssd1306_swap(x, y);
x = WIDTH - x - 1;
x -= (h - 1);
break;
case 2:
// 180 degree rotation, invert x and y, then shift y around for height.
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
y -= (h - 1);
break;
case 3:
// 270 degree rotation, swap x & y for rotation, then invert y
bSwap = true;
ssd1306_swap(x, y);
y = HEIGHT - y - 1;
break;
}
if (bSwap)
drawFastHLineInternal(x, y, h, color);
else
drawFastVLineInternal(x, y, h, color);
}
/*!
@brief Draw a vertical line with a width and color. Used by public method
drawFastHLine,drawFastVLine
@param x
Leftmost column -- 0 at left to (screen width - 1) at right.
@param __y
Row of display -- 0 at top to (screen height -1) at bottom.
@param __h height of the line in pixels
@param color
Line color, one of: SSD1306_BLACK, SSD1306_WHITE or
SSD1306_INVERSE.
@return None (void).
@note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display(), or with other graphics
commands as needed by one's own application.
*/
void Adafruit_SSD1306::drawFastVLineInternal(int16_t x, int16_t __y,
int16_t __h, uint16_t color) {
if ((x >= 0) && (x < WIDTH)) { // X coord in bounds?
if (__y < 0) { // Clip top
__h += __y;
__y = 0;
}
if ((__y + __h) > HEIGHT) { // Clip bottom
__h = (HEIGHT - __y);
}
if (__h > 0) { // Proceed only if height is now positive
// this display doesn't need ints for coordinates,
// use local byte registers for faster juggling
uint8_t y = __y, h = __h;
uint8_t *pBuf = &buffer[(y / 8) * WIDTH + x];
// do the first partial byte, if necessary - this requires some masking
uint8_t mod = (y & 7);
if (mod) {
// mask off the high n bits we want to set
mod = 8 - mod;
// note - lookup table results in a nearly 10% performance
// improvement in fill* functions
// uint8_t mask = ~(0xFF >> mod);
static const uint8_t PROGMEM premask[8] = {0x00, 0x80, 0xC0, 0xE0,
0xF0, 0xF8, 0xFC, 0xFE};
uint8_t mask = pgm_read_byte(&premask[mod]);
// adjust the mask if we're not going to reach the end of this byte
if (h < mod)
mask &= (0XFF >> (mod - h));
switch (color) {
case SSD1306_WHITE:
*pBuf |= mask;
break;
case SSD1306_BLACK:
*pBuf &= ~mask;
break;
case SSD1306_INVERSE:
*pBuf ^= mask;
break;
}
pBuf += WIDTH;
}
if (h >= mod) { // More to go?
h -= mod;
// Write solid bytes while we can - effectively 8 rows at a time
if (h >= 8) {
if (color == SSD1306_INVERSE) {
// separate copy of the code so we don't impact performance of
// black/white write version with an extra comparison per loop
do {
*pBuf ^= 0xFF; // Invert byte
pBuf += WIDTH; // Advance pointer 8 rows
h -= 8; // Subtract 8 rows from height
} while (h >= 8);
} else {
// store a local value to work with
uint8_t val = (color != SSD1306_BLACK) ? 255 : 0;
do {
*pBuf = val; // Set byte
pBuf += WIDTH; // Advance pointer 8 rows
h -= 8; // Subtract 8 rows from height
} while (h >= 8);
}
}
if (h) { // Do the final partial byte, if necessary
mod = h & 7;
// this time we want to mask the low bits of the byte,
// vs the high bits we did above
// uint8_t mask = (1 << mod) - 1;
// note - lookup table results in a nearly 10% performance
// improvement in fill* functions
static const uint8_t PROGMEM postmask[8] = {0x00, 0x01, 0x03, 0x07,
0x0F, 0x1F, 0x3F, 0x7F};
uint8_t mask = pgm_read_byte(&postmask[mod]);
switch (color) {
case SSD1306_WHITE:
*pBuf |= mask;
break;
case SSD1306_BLACK:
*pBuf &= ~mask;
break;
case SSD1306_INVERSE:
*pBuf ^= mask;
break;
}
}
}
} // endif positive height
} // endif x in bounds
}
/*!
@brief Return color of a single pixel in display buffer.
@param x
Column of display -- 0 at left to (screen width - 1) at right.
@param y
Row of display -- 0 at top to (screen height -1) at bottom.
@return true if pixel is set (usually SSD1306_WHITE, unless display invert
mode is enabled), false if clear (SSD1306_BLACK).
@note Reads from buffer contents; may not reflect current contents of
screen if display() has not been called.
*/
bool Adafruit_SSD1306::getPixel(int16_t x, int16_t y) {
if ((x >= 0) && (x < width()) && (y >= 0) && (y < height())) {
// Pixel is in-bounds. Rotate coordinates if needed.
switch (getRotation()) {
case 1:
ssd1306_swap(x, y);
x = WIDTH - x - 1;
break;
case 2:
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
break;
case 3:
ssd1306_swap(x, y);
y = HEIGHT - y - 1;
break;
}
return (buffer[x + (y / 8) * WIDTH] & (1 << (y & 7)));
}
return false; // Pixel out of bounds
}