项目开发实录(一):基于RDK X5的智能垃圾分类垃圾桶

文章目录

    • 项目简介
    • 硬件及材料列表
    • 整体架构流程
    • 技术细节
    • 后续开发安排
    • -----------------------------分割线----------------------------------

项目简介

基于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等多种复杂模型和最新算法,加速智能化应用快速落地。

硬件及材料列表

  1. 开发板与处理器
    RDK X5开发板:具备10 TOPS的算力,支持多种AI模型和丰富的接口.
  2. 摄像头模块
    轮趣科技的新版C100带金属外壳超大FOV相机:用于采集垃圾图像。
    或者:RDK X5 MIPI摄像头:适用于RDK X5开发板的摄像头模块。
  3. 传感器:
    HC-SR04超声波传感器:检测垃圾桶盖子的闭合状态。
  4. 舵机
    控制垃圾桶的盖子的开合动作。
    SG90舵机:使用最广泛的舵机,性价比高。由于本人只是制作模型,所以选择这个舵机。
    DS3115舵机:可控角度180°,15KG扭矩,带动托盘转动。
    DS3218舵机:可控角度360°,20KG扭矩,带动旋转圆盘转动。
  5. 电源
    可以选择5V锂电池供电或者220V交流转直流电源模块供电。如果采用DS3115舵机,那么它的工作电压是5V,RDK开发板的工作电压也是5V,但是需要保证电流的供应。
  6. 显示屏
    使用任意小型的带HDMI接口的显示屏即可。
    或者使用RaspberryPi 3.5寸电容USB触摸显示屏,本质上RDK与树莓派没什么差别,接口位置也类似。
  7. 其他配件
    铝合金外壳:RDK X5铝合金外壳。或者 散热扩展板:用于RDK X5的散热。
    LED灯:用于指示分类结果或状态。
    按键:用于与用户进行交互,当分类结果错误时,手动更正。
    红外对射管:用于检测垃圾满溢状态。

整体架构流程

工作流程
以下是基于RDK X5的智能垃圾分类垃圾桶垃圾分类的详细流程:

  1. 图像识别触发:用户将垃圾放置在垃圾桶的摄像头前,摄像头捕获垃圾图像。
  2. AI模型推理:摄像头捕获的图像被发送到RDK X5开发板,AI模型对图像进行实时分析和识别,确定垃圾的类型。
  3. 控制信号生成:一旦AI模型识别出垃圾类型,它会生成一个控制信号,这个信号会指示哪个垃圾桶的盖子需要打开。
  4. 舵机接收信号:控制信号通过GPIO接口发送给对应的舵机控制器。
  5. 舵机动作执行:舵机接收到控制信号后,根据预设的角度和速度转动,打开对应垃圾桶的盖子。如果垃圾桶设计为多个独立的部分,每个部分都可能有一个或多个舵机控制其盖子的开合。如果分类错误,还可以使用按钮手动控制开合。
  6. 垃圾投放:用户在收到指示(如LED灯或语音提示)后,将垃圾投放到指定的垃圾桶中。
  7. 盖子关闭:垃圾投放完成后(例如等待5-10秒钟),舵机会再次转动,关闭垃圾桶的盖子。
  8. 反馈确认:系统可能会提供反馈确认,如通过LED灯闪烁或语音提示,告知用户垃圾分类完成。

硬件架构
以下是项目的硬件架构。

软件架构
项目开发实录(一):基于RDK X5的智能垃圾分类垃圾桶_第1张图片
流程图解释:
开始:软件流程的起点。
图像采集:系统通过摄像头模块采集垃圾图像。
图像预处理:对采集到的图像进行必要的预处理,如缩放、裁剪、标准化等,以适配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模型,所以我们也需要再保存成这个格式,以便后面能够引入。

后面我将把模型部署在平台上。

感谢大家的关注,欢迎留言交流!

你可能感兴趣的:(RDK,X5,地瓜机器人,分类,人工智能)