基于jetson nano的图像识别智能垃圾桶

项目介绍

此项目使用jetson nano和STM32开发板完成,jetson nano通过摄像头获取垃圾图像,并使用AI模型进行推理,得到推理结果,发送指令控制STM32操作对应类别的舵机,从而打开对应垃圾类别的垃圾桶

硬件准备

硬件材料准备

  • Nvidia Jetson nano开发板 4GB内存
  • STM32F103C8T6开发板
  • 鼠标
  • 键盘
  • 显示器
  • CSI摄像头
  • SD卡与读卡器
  • MG90舵机(4个)
  • 超声波测距传感器HC-SR04
  • 迷你垃圾桶(4个)
  • 杜邦线若干

硬件连接简图

在这里插入图片描述

环境搭建

系统烧录与初始化设置

首先,我们需要准备好我们的SD卡和读卡器,将其插入我们的PC端,使用SD Card Formatter将其初始化

在这里插入图片描述

再使用belenaEtcher将我们从英伟达官网下载的系统镜像下载至SD卡

在这里插入图片描述
在这里插入图片描述

此过程耗时较长,请耐心等待

注:在烧录完成以后,会有验证过程,在验证过程中,你的电脑不断出现新的磁盘,提示你进行格式化,直接点击取消即可,烧录到最后,烧录软件会提示你烧录失败,但实际上已经烧录成功了,不必在意提示

此系统镜像的链接如下:https://developer.nvidia.com/embedded/jetpack-sdk-44-archive

在这里插入图片描述

点击最左边框中“Download the SD Card Image“即可下载,本项目使用的是JetPack4.4,如果你要下载其他版本的镜像,可在如下链接中寻找自己想要的版本:https://developer.nvidia.com/embedded/jetpack-archive

将SD插入卡槽,将Jetson nano与键盘,鼠标和显示器相连接,插上电源,启动系统,开机以后要求填入用户名,设置密码等初始化操作,直接按部就班完成即可,设置完成以后系统会自动重启,至此完成了系统安装

jupyter-lab与相关依赖安装

首先需要安装nodejs和npm:

在这里插入图片描述

如果要安装其他版本的nodejs,可自行前往官网选择:https://nodejs.org/dist/

安装好以后node 以后,使用软链接将node和npm链接至系统的bin目录下,查看Node版本:

sudo ln -s /usr/local/"你下载的node所在文件夹的名称"/bin/node /usr/local/bin
sudo ln -s /usr/local/"你下载的node所在文件夹的名称"/bin/npm /usr/local/bin
node -v

由于下载的版本不同,保存node所在的文件夹名称也有所不同,这里根据你自己下载的版本来确定,

在这里插入图片描述

安装Node成功

安装libffi-dev:

sudo apt-get install libffi-dev

安装packaging:

pip3 install packaging

安装setuptools:

pip3 install setuptools

安装jupyter-lab,我安装的版本是2.2.6,注意,此处不要安装最新版本的jupyter-lab,因为最新版本的jupyter-lab要求nodejs版本>=12.x.x,而此版本的nodejs又会要求ubuntu系统给glibc版本为2.28,而JetPack为我们提供的系统镜像为ubuntu 18.04,glibc版本最高为2.27,而升级此系统库容易使系统崩溃,从而导致系统重装,为了避免出现某些奇怪报错,建议所有软件版本严格参考此博客进行下载

pip3 install jupyter-lab=2.2.6

在这里插入图片描述

出现报错,经尝试,解决方案使升级pip,

python3 -m pip install --upgrade pip

在这里插入图片描述

升级完成,再次安装Jupyter-lab,成功

在这里插入图片描述

查看jupyter --version

发现还有部分依赖,如ipywidgets,jupyter_server等没有安装,直接使用Pip无脑安装即可

安装完成以后如下:

在这里插入图片描述

安装完成以后,还需要对jupyter lab进行设置,首先启用拓展

jupyter nbextension enable --py widgetsnbextension
jupyter labextension install @jupyter-widgets/jupyterlab-manager

安装完成以后,使用命令生成配置文件

jupyter lab --generate-config

在这里插入图片描述

找到相关目录下的配置文件,使用vim 编辑器打开jupyter_notebook_config.py文件,添加以下内容

c.ServerApp.allow_remote_access=True    //允许远程访问
c.ServerApp.ip='0.0.0.0'				//允许所有IP访问
c.ServerApp.open_browser=False			//默认不打开浏览器
c.ServerApp.allow_root=True				//允许使用root用户运行jupyterlab
c.ServerApp.port=8888					//设置端口
c.ServerApp.notebook_dir='home/fatshark'//设置工作目录

在这里插入图片描述

在终端输入命令,运行jupyter lab

jupyter lab

打开浏览器,输入localhost:8888

在这里插入图片描述

如果要远程登录,可以在其他主机浏览器中输入IP地址:端口号即可

pytorch安装

在Jetson nano上面安装的pytorch与我们平常在PC端主机安装的pytorch不同,因为Jetson nano是ARM架构的,pytorch官网提供的均是x86架构的,所以要参考Nvidia官网提供的指示来安装ARM架构的pytorch

链接如下https://forums.developer.nvidia.com/t/pytorch-for-jetson/72048

在这里插入图片描述

主页有很多版本的PyTorch,针对JetPack4.4,我们选择v1.10.0

具体安装可以查看Instructions

在这里插入图片描述

针对Python3.6,在命令行终端输入以下命令即可

注意,上图中只是给了示例,其中显示的版本号不一定是我们需要的,我们需要将命令中的版本改为自己实际需要的版本,上图中安装的版本是1.8.0,我们实际安装的是1.10.0,注意区别,torchvision的安装也同理

安装pytorch,注意版本为1.10.0

wget https://nvidia.box.com/shared/static/p57jwntv436lfrd78inwl7iml6p13fzh.whl -O torch-1.10.0-cp36-cp36m-linux_aarch64.whl
sudo apt-get install python3-pip libopenblas-base libopenmpi-dev libomp-dev
pip3 install 'Cython<3'
pip3 install numpy torch-1.10.0-cp36-cp36m-linux_aarch64.whl

安装torchvision,注意版本为0.11.1

$ sudo apt-get install libjpeg-dev zlib1g-dev libpython3-dev libopenblas-dev libavcodec-dev libavformat-dev libswscale-dev
$ git clone --branch <version> https://github.com/pytorch/vision torchvision   # see below for version of torchvision to download
$ cd torchvision
$ export BUILD_VERSION=0.11.1  # where 0.x.0 is the torchvision version  
$ python3 setup.py install --user
$ cd ../  # attempting to load torchvision from build dir will result in import error
$ pip install 'pillow<7' # always needed for Python 2.7, not needed torchvision v0.5.0+ with Python 3.6

安装完成以后,在终端输入python3,尝试import torch和torchvision,同时打印出对应的版本

python3
>>> import torch
>>> import torchvision
>>> print(torch.__version__)
>>> print(torchvision.__version__)

如果能出现版本号,则证明已经安装成功

STM32开发环境搭建

STM32的开发需要用到Keil和STM32CubeMx,

因涉及到软件版权问题,Keil和STM32CubeMX的安装过程,此博客不做详细介绍,可以自行阅读其他博客进行学习

使用jupyter-lab创建交互界面

在jupyter-lab中创建交互界面,主要是使用ipywidgets组件来完成,其中主要使用到的有Button,IntText,Dropdown等组件

catgo_widget = ipywidgets.Dropdown(options=CATEGORIES, description = 'category')
add_data_btn = ipywidgets.Button(description='add img')
delete_data_btn = ipywidgets.Button(description='del imgs')
count_widget = ipywidgets.IntText(description = 'count:',value=0,disabled = True)
count_widget.value = datasets.get_count(catgo_widget.value)

# 得到数据集中,每一个类别的数据集大小
def cat_count(c):
    count_widget.value = datasets.get_count(catgo_widget.value)
catgo_widget.observe(cat_count,names='value')

# 往数据集中添加图像
def add_images(c):
    datasets.save_entry(camera.value,catgo_widget.value)
    count_widget.value = datasets.get_count(catgo_widget.value)
add_data_btn.on_click(add_images)

#删除图像
def del_images(c):
    datesets.delete_imgs(catgo_widget.value)
delete_data_btn.on_click(del_images)

data_collection_widget = ipywidgets.VBox([
    ipywidgets.HBox([image_widget]), catgo_widget, count_widget, add_data_btn,delete_data_btn
])

对于Button组件,主要使用on_click方法来触发回调函数,进行操作,对于Dropdown(下拉菜单),主要使用observe方法对其中的属性值进行监听。此代码块仅为交互界面部分代码,详细内容见文末

此交互界面最终完整效果如下:

在这里插入图片描述

此交互界面主要包括摄像头实时画面,数据集添加界面,模型训练界面,模型保存和读取界面以及模型推理界面

模型训练

我们使用预训练的模型resnet-18来作为图像识别要用到的模型

引入模型:

import torch
import torchvision

device = torch.device('cuda')# 使用cuda

# RESNET 18
model = torchvision.models.resnet18(pretrained=True)# 预训练
model.fc = torch.nn.Linear(512, len(datasets.categories))
    
model = model.to(device)

训练和评估模型:

def train_eval(is_training):
    global BATCH_SIZE, LEARNING_RATE, MOMENTUM, model, datasets, optimizer, eval_button, train_button, accuracy_widget, loss_widget, progress_widget, state_widget,info_widget# 一些全局变量
    
    try:
        train_loader = torch.utils.data.DataLoader(
            datasets,
            batch_size=BATCH_SIZE,# BATCH_SIZE为8
            shuffle=True
        )
        
        state_widget.value = 'stop'
        train_button.disabled = True
        eval_button.disabled = True
        time.sleep(1)

        if is_training:
            model = model.train()
        else:
            model = model.eval()
        while epochs_widget.value > 0:
            i = 0
            sum_loss = 0.0
            error_count = 0.0
            for images, labels in iter(train_loader):
                # 将数据传输到GPU上
                images = images.to(device)
                labels = labels.to(device)

                if is_training:
                    # 将梯度清0
                    optimizer.zero_grad()

                # 进行推理,得到结果
                outputs = model(images)

                # 计算误差
                loss = F.cross_entropy(outputs, labels)

                if is_training:
                    # 进行反向传播
                    loss.backward()

                    # 调整权重
                    optimizer.step()

                # 对推理结果进行统计,计算准确率
                error_count += len(torch.nonzero(outputs.argmax(1) - labels).flatten())
                count = len(labels.flatten())
                i += count
                sum_loss += float(loss)
                progress_widget.value = i / len(datasets)
                loss_widget.value = sum_loss / i
                accuracy_widget.value = 1.0 - error_count / i
                
            if is_training:
                epochs_widget.value = epochs_widget.value - 1
            else:
                break
    except e:
        info_widget.value='an error occured'
    model = model.eval()

此代码为训练时运行的核心代码,train_eval为主要函数,train_eval中会接收一个参数,用于确认此次执行的行为是训练模型还是评估模型,在训练时,会根据结果进行反向传播,调整权重,评估模型时仅仅计算误差与准确率。

STM32相关驱动

在STM32部分,需要用到的硬件有USART和PWM接口,此外还需要两根引脚用于驱动超声波测距模块,PWM信号的输出我们计划使用TIM1的CH1,CH2,CH3,CH4通道,USART使用USART2->RX和USART3->TX(之所以不直接使用同一个USART的RX和TX,单纯是因为硬件接线不方便,所以这里使用了两个USART的不同功能引脚)

此外,对于超声波传感器,使用PA0和PA1作为驱动接口,PA0使用TIM2_CH1作为获取传感器数据的引脚,PA1作为触发超声波传感器工作的引脚

解释一下为什么使用超声波传感器,在此项目中,AI模型要解决的问题是一个四分类问题,那么意味着,我们获取到的图像中,无论是否存在垃圾桶,AI模型都会进行推理并得到一个结果,这明显不符合系统的工作逻辑,我们希望添加一些限制条件,才会触发AI模型进行推理,所以,我们使用超声波传感器,只有当垃圾在摄像头前一定距离的时候,我们才进行推理

根据超声波模块的工作原理图:

在这里插入图片描述

这里Trig为PA1,Echo为PA0

超声波驱动代码如下:

void HC_Start(void)//启动传感器进行一次测距
{
	HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);
	Delay(1000);
	HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);
	Delay(100000);
}
uint32_t Distance=0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM2)
	{
		if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)//捕获到上升沿时,清空CNT
		{
			__HAL_TIM_SetCounter(htim,0);
		}
		if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_2)
		{
			Distance = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);//捕获到下降沿时,读取捕获值
		}
		HAL_TIM_IC_Start(htim,TIM_CHANNEL_1);
		HAL_TIM_IC_Start(htim,TIM_CHANNEL_2);
	}
}

在获取echo值时,我们使用到了TIM2_CH1的输入捕获功能,在第一次捕获到上升沿时,将CNT清0,那么在第二次捕获到下降沿时,就可以直接得到高电平的持续时间,从而方便地算出距离

主函数核心代码如下:

	HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);//开启PWM输出
	HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
	HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
	HAL_UARTEx_ReceiveToIdle_IT(&huart2,(uint8_t *)Motor_Cmd,10);//使用串口接收推理数据
	
	HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//开启输入捕获
	HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_2);
	
	__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_1,CLOSE);//初始化时,先关闭垃圾桶
	__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_2,CLOSE);
	__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_3,CLOSE);
	__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_4,CLOSE);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
	uint32_t sum=0;//进行均值滤波的中间变量
	uint8_t motor_flag=0;//用于判断是否接收到推理结果
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		
		uint8_t i=10;
		while(i--)
		{
			HC_Start();//激活HC传感器进行测量
			HAL_Delay(10);
			sum+=Distance;
		}
		sum=sum/10;
		if(sum<350)//垃圾距离足够近,才开启系统工作
		{
			motor_flag=1;
		}
		else if(sum>450)//距离太远,整个系统不工作
		{
			motor_flag=0;
		}
		sum=0;
		HAL_Delay(400);
		if(motor_flag==1)//垃圾在合适的距离内才进行推理
		{
			if(Serial_flag==1)
			{
				Motor_Ctrl();//根据推理结果控制对应垃圾桶开关
				for(uint8_t i=0 ; i < 10; i++ )
				{
					Motor_Cmd[i]='\0';
				}
				Serial_flag=0;
			}
			HAL_UART_Transmit(&huart3, (uint8_t *)"start", 5, 50);//开启
		}
		else//距离过远,停止推理,同时垃圾桶关闭
		{
			HAL_UART_Transmit(&huart3, (uint8_t *)"stop", 5, 50);//关闭
			__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_1,CLOSE);
			__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_2,CLOSE);
			__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_3,CLOSE);
			__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_4,CLOSE);
		}

在控制舵机的核心代码中,我们需要根据超声波得到的距离来判断我们是否应该打开垃圾桶,只有距离小于一定值时,我们才会执行推理,控制垃圾桶开关,否则垃圾桶应一直保持关闭状态

功能集成

在搞定了AI模型以及STM32相关驱动后,我们将进行功能集成,把各个组件全部连接,进行测试

在功能集成部分,需要使用到Jetson nano的串口功能,我们需要使用串口将我们的推理结果发送至STM32,从而确定打开哪一个垃圾桶,同时,Jetson nano也应该接收来自STM32的距离信息,根据距离信息判断是否进行推理,

在发送推理结果时,我们会发送一个字符串,该字符串由四个为’0’或’1’的字符构成,分别代表了四个舵机的开关状态,如果发送的字符串是’0100’,表明我们应该打开二号舵机,STM32在接收到此字符串后,会进行解析,并打开对应舵机。

同时,我们也应该使用STM32发送超声波传感器的距离信息,从而确认是否进行推理,如下图:

在这里插入图片描述

核心代码如下:

import threading
import time
from utils import preprocess
import torch.nn.functional as F
import serial as ser
#my code begin:
se = ser.Serial("/dev/ttyTHS1",9600,timeout=50)
motor_cmd=['0','0','0','0']

# new:
dis_cmd=b'123'
def ser_func():#串口接收信息
    global dis_cmd
    while True:
        time.sleep(0.1)
        if se.in_waiting>0:
            dis_cmd = se.read_all()

ser_thread = threading.Thread(target=ser_func)
ser_thread.start()

#my code end
state_widget = ipywidgets.ToggleButtons(options=['stop', 'start'], description='state', value='stop')
prediction_widget = ipywidgets.Text(description='prediction')
score_widgets = []
for category in datasets.categories:
    score_widget = ipywidgets.FloatSlider(min=0.0, max=1.0, description=category, orientation='vertical')
    score_widgets.append(score_widget)

def live(state_widget, model, camera, prediction_widget, score_widget):
    global datasets,motor_cmd, score_widgets,dis_cmd
    print("we are in the live")
    while state_widget.value == 'start':
        if dis_cmd==b'start':#距离小于一定值,开始推理
            image = camera.value
            preprocessed = preprocess(image)
            output = model(preprocessed)
            output = F.softmax(output, dim=1).detach().cpu().numpy().flatten()
            category_index = output.argmax()
            #my code:
            time.sleep(0.01)
            for i in range(0,4):
                if(category_index==i):
                    motor_cmd[i]='1'
                else:
                    motor_cmd[i]='0'
            new = ''.join(motor_cmd)
            se.write(new.encode("GBK"))# 发送推理信息,确认打开对应舵机
            prediction_widget.value = datasets.categories[category_index]
            for i, score in enumerate(list(output)):
                score_widgets[i].value = score
        else:
            se.write("0000".encode("GBK"))# 距离太远,所有舵机均为关闭
            score_widgets[0].value=0.0
            score_widgets[1].value=0.0
            score_widgets[2].value=0.0
            score_widgets[3].value=0.0
    se.write("0000".encode("GBK"))# 停止状态,关闭所有舵机
    score_widgets[0].value=0.0
    score_widgets[1].value=0.0
    score_widgets[2].value=0.0
    score_widgets[3].value=0.0

此代码块主要解决了串口信息的收发,以及对推理结果的处理

完整工程代码详见github链接:

  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值