基于RDK X5开发板的智能垃圾分类垃圾桶项目,旨在利用人工智能技术实现垃圾的自动识别与分类。垃圾桶硬件装置应实现对行人投入垃圾的四分类投放(可回收垃圾、有害垃圾、厨余垃圾、其他垃圾)。该系统主要由摄像头模块、RDK X5开发板和分类装置组成。摄像头通过MIPI CSI接口或者USB接口连接,实时采集垃圾图像;RDK X5开发板凭借其10 TOPS的算力,运行预先训练的深度学习模型,对图像进行分析,识别垃圾类型;根据识别结果,开发板通过GPIO接口控制舵机,打开对应垃圾桶的盖子,实现垃圾的自动分类。该方案充分利用RDK X5的强大计算能力和丰富接口,确保系统的高效性和可靠性。
背景介绍:随着垃圾分类的重要性日益增加,自动化的智能垃圾分类解决方案在城市管理、家庭和公共设施中具有广泛的应用前景。通过AI技术,能够有效识别垃圾类型,减少分类错误,提升垃圾回收效率,并为环保和资源再利用做出积极贡献。RDK X5开发板具备10 TOPS的算力和多种接口,非常适合实时图像识别、自动化控制以及与其他物联网设备的集成,能够在垃圾分类、智慧城市建设中发挥关键作用。
RDK X5:D-Robotics RDK X5搭载Sunrise 5智能计算芯片,可提供高达10 Tops的算力,是一款面向智能计算与机器人应用的全能开发套件,接口丰富,极致易用,支持Transfomer、RWKV、Occupancy、Stereo Perception等多种复杂模型和最新算法,加速智能化应用快速落地。
工作流程
以下是基于RDK X5的智能垃圾分类垃圾桶垃圾分类的详细流程:
硬件架构
以下是项目的硬件架构。
软件架构
流程图解释:
开始:软件流程的起点。
图像采集:系统通过摄像头模块采集垃圾图像。
图像预处理:对采集到的图像进行必要的预处理,如缩放、裁剪、标准化等,以适配AI模型的输入要求。
AI模型推理:将预处理后的图像输入到AI模型中,进行推理分析。
垃圾类型识别:AI模型识别出垃圾的类型。
是:如果识别成功,流程进入生成控制信号阶段。
否:如果识别失败或不确定,流程将转入手动按钮选择阶段。
手动按钮选择:用户可以通过手动按钮选择开启哪个对应的垃圾桶,绕过AI识别直接进行分类。
生成控制信号:根据识别结果或手动选择,系统生成控制信号。
舵机控制:系统通过GPIO接口发送控制信号给舵机。
打开对应垃圾桶盖:舵机根据控制信号打开对应垃圾桶的盖子。
用户投放垃圾:用户根据指示将垃圾投放到打开的垃圾桶中。
红外对射管检测:使用红外对射管检测垃圾是否满溢。
满:如果检测到垃圾桶满溢,扬声器会发出报警声音。
未满:如果未满,关闭垃圾桶盖并继续流程。
扬声器报警:当垃圾桶满溢时,扬声器发出报警声音提示。
关闭垃圾桶盖:投放垃圾后,舵机动作关闭垃圾桶盖。
HDMI屏幕显示结果:通过HDMI屏幕显示垃圾分类的结果。
结束:软件流程的终点。
编程语言使用python3.10
深度学习框架使用pytorch2.5.0
pytorch模型
使用PyTorch训练一个垃圾分类模型。以下是简化的代码,数据集地址采用GitHub上开源的:https://github.com/garythung/trashnet
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, models, transforms
from torch.utils.data import DataLoader
# 数据预处理
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
data_dir = 'path_to_your_dataset'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x])
for x in ['train', 'val']}
dataloaders = {x: DataLoader(image_datasets[x], batch_size=32,
shuffle=True, num_workers=4)
for x in ['train', 'val']}
# 加载预训练模型并修改最后的全连接层
model = models.resnet18(pretrained=True)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, num_classes) # num_classes为您的分类数
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# 训练模型
num_epochs = 25
for epoch in range(num_epochs):
for phase in ['train', 'val']:
if phase == 'train':
model.train()
else:
model.eval()
running_loss = 0.0
running_corrects = 0
for inputs, labels in dataloaders[phase]:
optimizer.zero_grad()
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
if phase == 'train':
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / len(image_datasets[phase])
epoch_acc = running_corrects.double() / len(image_datasets[phase])
print(f'Epoch {epoch}/{num_epochs - 1} | {phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')
# 保存模型
torch.save(model.state_dict(), 'garbage_classification_model.pth')
有了模型之后,我们可以结合RDK X5给的官方python用法的示例,进行继续开发。
图像采集与识别
简化版的代码,该代码将读取USB摄像头中的图像,使用模型进行识别,并将结果输出到garbage_identification函数,这个函数可以在后续被程序识别,然后控制舵机带动垃圾桶的盖板开启或关闭。
#!/usr/bin/env python3
import sys
import signal
import cv2
from hobot_dnn import pyeasy_dnn as dnn
import numpy as np
import ctypes
import json
# 定义信号处理函数
def signal_handler(signal, frame):
print("\nExiting program")
sys.exit(0)
# 加载模型
def load_model(model_path):
return dnn.load(model_path)
# 将BGR格式图片转换成 NV12格式
def bgr2nv12_opencv(image):
height, width = image.shape[0], image.shape[1]
area = height * width
yuv420p = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420).reshape((area * 3 // 2,))
y = yuv420p[:area]
uv_planar = yuv420p[area:].reshape((2, area // 4))
uv_packed = uv_planar.transpose((1, 0)).reshape((area // 2,))
nv12 = np.zeros_like(yuv420p)
nv12[:height * width] = y
nv12[height * width:] = uv_packed
return nv12
# 使用模型进行识别
def garbage_identification(image, model):
ret, frame = image.read()
if not ret:
print("Failed to get image from USB camera")
return None
# 图像预处理
h, w = model.inputs[0].properties.shape[2], model.inputs[0].properties.shape[3]
des_dim = (w, h)
resized_data = cv2.resize(frame, des_dim, interpolation=cv2.INTER_AREA)
# 将BGR格式图片转换成NV12格式
nv12_data = bgr2nv12_opencv(resized_data)
# 模型推理
outputs = model.forward(nv12_data)
# 后处理,这里需要根据自己的模型输出进行调整
# 假设模型输出是一个JSON字符串
result_str = json.dumps({"detection": outputs})
return json.loads(result_str)
# 主函数
if __name__ == '__main__':
signal.signal(signal.SIGINT, signal_handler)
# 加载模型
model_path = '/path/to/your/model.bin' # 替换为模型路径
model = load_model(model_path)
# 获取USB摄像头设备
video_device = find_first_usb_camera()
if video_device is None:
print("No USB camera found.")
sys.exit(-1)
# 打开USB摄像头
cap = cv2.VideoCapture(video_device)
if not cap.isOpened():
exit(-1)
while True:
# 使用模型进行识别
recognition_result = garbage_identification(cap, model)
if recognition_result is not None:
# 处理识别结果
for detection in recognition_result.get("detection", []):
print("Detected:", detection['name'], "with confidence:", detection['score'])
# 这里可以添加代码来控制电机和LED等
# 按需添加延时或等待下一帧
cv2.waitKey(1)
驱动舵机运行:
舵机接收来自控制器的脉宽调制(PWM)信号,信号的脉宽对应目标角度。
以下是一个简化的示例,展示了如何实现当识别到“干垃圾”时,产生PWM波驱动电机,并且控制对应垃圾桶的LED灯点亮。当无法识别垃圾时,提示用户手动按钮控制垃圾的分类。
#!/usr/bin/env python3
import sys
import signal
import Hobot.GPIO as GPIO
import time
# 定义使用的GPIO通道
led_pin = 36 # BOARD 编码 36,用于控制LED
motor_pin = 33 # BOARD 编码 33,用于PWM控制电机
but_pin = 37 # BOARD 编码 37,用于接收按钮输入
# 禁用警告信息
GPIO.setwarnings(False)
# 设置管脚编码模式为硬件编号 BOARD
GPIO.setmode(GPIO.BOARD)
# 初始化GPIO
GPIO.setup(led_pin, GPIO.OUT) # LED pin set as output
GPIO.setup(motor_pin, GPIO.OUT) # Motor pin set for PWM
GPIO.setup(but_pin, GPIO.IN) # Button pin set as input
# 初始化PWM
pwm = GPIO.PWM(motor_pin, 48000) # 48KHz frequency
pwm.start(0) # Start PWM with 0% duty cycle
def signal_handler(signal, frame):
sys.exit(0)
def manual_button_control():
# 等待按钮按下
print("Please press the button to manually select the垃圾分类.")
GPIO.wait_for_edge(but_pin, GPIO.FALLING)
print("Button Pressed! Manual selection received.")
def garbage_identification():
# 这里应该是调用AI模型进行垃圾识别的代码
# 为了简化,我们假设如果返回"干垃圾",则表示识别成功
return "干垃圾"
def main():
try:
while True:
# 垃圾识别
garbage_type = garbage_identification()
if garbage_type == "干垃圾":
# 识别成功,控制电机和LED
pwm.ChangeDutyCycle(50) # 改变占空比到50%
GPIO.output(led_pin, GPIO.HIGH) # 点亮LED
time.sleep(5) # 保持5秒
pwm.ChangeDutyCycle(0) # 停止PWM
GPIO.output(led_pin, GPIO.LOW) # 熄灭LED
else:
# 识别失败,提示用户手动选择
manual_button_control()
finally:
pwm.ChangeDutyCycle(0) # 改变占空比到0% 关闭垃圾桶
pwm.stop() # 停止PWM
GPIO.cleanup() # 清理所有GPIO
if __name__ == '__main__':
signal.signal(signal.SIGINT, signal_handler)
main()
其中,垃圾识别:garbage_identification函数模拟垃圾识别过程,实际应用中应替换为调用AI模型的代码。
RDK X5支持ONNX格式的模型,但是部署也存在一些问题,后续得参考开发者社区和官方文档。
由于深度学习模型的训练通常需要高性能的计算资源,本人只有一个1650显卡,训练有点力不从心。后面将在谷歌的colab或者百度飞桨上租用一台,用来训练尝试。为了获取足够的训练数据,我参考了TrashNet数据集,并对其进行了预处理。训练过程中,我调整了模型的超参数,最终实现了对干垃圾、湿垃圾、有害垃圾和可回收垃圾的准确分类。后面将结合ROS2,利用ROS2的节点机制,将各个功能模块独立开发,并通过话题(topic)进行通信。例如,摄像头节点负责图像采集,推理节点负责模型推理,控制节点负责舵机和LED的控制。这种模块化设计提高了系统的可维护性和扩展性。
2024年11月24日更新
训练一个简单的模型,我是利用pytorch + notebook + pycharm来运行的,可以利用conda先安装notebook,然后配置好环境就能直接在pycharm中启动notebook了。
首先检查以下运行环境怎么样。
import torch
print(torch.__version__)
print(torch.cuda.is_available())
print(torch.version.cuda)
2.5.1
True
12.4
import os
dataset_path = "F:/fenlei/train" # 这里各位根据自己的目录替换掉
print(os.listdir(dataset_path)) # 列出数据集中顶层文件夹
from torchvision import datasets, transforms
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
])
dataset = datasets.ImageFolder(root=dataset_path, transform=transform)
# 打印类别和样本数量
print(f"Classes: {dataset.classes}") # 列出类别名称
print(f"Class-to-Index Mapping: {dataset.class_to_idx}") # 类别到索引的映射
print(f"Total Images: {len(dataset)}") # 数据集中图片总数
[‘Harmful’, ‘Kitchen’, ‘Other’, ‘Recyclable’]
Classes: [‘Harmful’, ‘Kitchen’, ‘Other’, ‘Recyclable’]
Class-to-Index Mapping: {‘Harmful’: 0, ‘Kitchen’: 1, ‘Other’: 2, ‘Recyclable’: 3}
Total Images: 64000
看样子数据成功引入了。
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
from torchvision.models import resnet18, ResNet18_Weights
from PIL import ImageFile
# 防止 PIL 处理某些截断的图像时出错
ImageFile.LOAD_TRUNCATED_IMAGES = True
# 数据预处理:调整图像大小、转换为张量、标准化
transform = transforms.Compose([
transforms.Resize((224, 224)), # 调整图像大小到 224x224
transforms.ToTensor(), # 转换为张量
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) # 对 RGB 通道进行归一化
])
# 数据集加载:从指定路径加载图像数据集,并应用预处理
dataset_path = "F:/fenlei/train" # 数据集路径
dataset = datasets.ImageFolder(root=dataset_path, transform=transform)
# 将数据集划分为训练集和验证集
train_size = int(0.8 * len(dataset)) # 训练集大小为 80%
val_size = len(dataset) - train_size # 验证集大小为剩余部分
train_set, val_set = random_split(dataset, [train_size, val_size]) # 随机划分数据集
# 加载器:将数据集分批次加载
train_loader = DataLoader(train_set, batch_size=32, shuffle=True) # 训练集加载器
val_loader = DataLoader(val_set, batch_size=32, shuffle=False) # 验证集加载器
# 加载预训练的 ResNet18 模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测是否有可用 GPU
model = resnet18(weights=ResNet18_Weights.DEFAULT) # 加载预训练权重
model.fc = nn.Linear(model.fc.in_features, 4) # 修改全连接层的输出为 4 类
model = model.to(device) # 将模型转移到设备(CPU 或 GPU)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 使用交叉熵损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # Adam 优化器,学习率为 0.001
# 开始训练
epochs = 10 # 设置训练轮数
for epoch in range(epochs):
model.train() # 将模型设置为训练模式
train_loss, correct = 0, 0 # 初始化训练损失和正确预测计数
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device) # 将图像和标签转移到设备
# 前向传播:计算模型输出
outputs = model(images)
loss = criterion(outputs, labels) # 计算损失
# 反向传播与优化
optimizer.zero_grad() # 清除上一轮的梯度
loss.backward() # 计算梯度
optimizer.step() # 更新模型参数
# 统计当前 batch 的损失和正确预测数量
train_loss += loss.item() * images.size(0) # 累加损失(按样本数量加权)
_, preds = torch.max(outputs, 1) # 获取预测类别
correct += (preds == labels).sum().item() # 统计正确预测数量
# 输出当前轮次的平均损失和准确率
print(f"Epoch [{epoch+1}/{epochs}], Loss: {train_loss/len(train_set):.4f}, Acc: {correct/len(train_set):.4f}")
选择使用 预训练的 ResNet18 模型,主要是因为如果从零开始训练一个深度神经网络,通常需要大量的数据。使用预训练模型可以利用已有的特征,减少对大规模数据集的依赖。ResNet18 采用残差结构,解决了深度网络训练中的梯度消失问题。
运行完之后,会输出类似训练的epoch 以及平均损失和准确率。
import os
from PIL import Image
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
import torch
# 自定义未标记数据集类
class UnlabeledDataset(Dataset):
def __init__(self, root_dir, transform=None):
self.root_dir = root_dir
self.transform = transform
self.image_paths = [os.path.join(root_dir, img) for img in os.listdir(root_dir) if img.endswith(('.jpg', '.png', '.jpeg'))]
def __len__(self):
return len(self.image_paths)
def __getitem__(self, idx):
img_path = self.image_paths[idx]
image = Image.open(img_path).convert("RGB")
if self.transform:
image = self.transform(image)
return image, img_path
# 路径和预处理
test_dataset_path = "F:/fenlei/eval"
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
])
# 创建测试数据加载器
test_dataset = UnlabeledDataset(root_dir=test_dataset_path, transform=transform)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
# 模型评估和预测
model.eval()
predictions = []
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
with torch.no_grad():
for images, image_paths in test_loader:
images = images.to(device)
outputs = model(images)
_, preds = torch.max(outputs, 1)
for img_path, pred in zip(image_paths, preds.cpu().numpy()):
predictions.append((img_path, pred))
from collections import Counter
# 假设 predictions 是 (img_path, pred) 的列表
# 提取所有的预测类别
predicted_classes = [pred for _, pred in predictions]
# 统计每个类别的数量
class_counts = Counter(predicted_classes)
# 打印每个类别的统计结果
for class_id, count in class_counts.items():
print(f"Class {class_id}: {count} images")
然后可以用验证数据集验证以下分类的效果。
# 保存模型
torch.save(model.state_dict(), "./model.pth")
# 导出为 ONNX
dummy_input = torch.randn(1, 3, 224, 224).to(device) # 模拟输入
torch.onnx.export(
model,
dummy_input,
"./model.onnx",
input_names=["input"],
output_names=["output"],
opset_version=11 # 使用适配 RDK X5 的 ONNX Opset 版本
)
import onnx
# 加载 ONNX 模型
onnx_model = onnx.load("./model.onnx")
onnx.checker.check_model(onnx_model) # 检查模型是否有效
print("ONNX Model is valid.")
由于RDK X5后面需要的是ONNX模型,所以我们也需要再保存成这个格式,以便后面能够引入。
后面我将把模型部署在平台上。
感谢大家的关注,欢迎留言交流!