树莓派和STM32智能垃圾桶分类全流程代码分享

目录

目录

前言

一、机械结构设计

附机械部分图纸​编辑

二、树莓派视觉识别

1.数据集的制作

(1)数据集处理部分

 (2)数据集的优化

2.数据集打标签建议

(1)网站推荐

(2)问题示例

3.树莓派环境搭建部分

(1)  YOLOv5-Lite环境的部署

4.具体Yolov5-lite算法部分

(1)引入库部分

(2)视觉识别主要部分代码

(3)树莓派控制舵机部分代码

三、淘晶池串口屏通讯代码

四、stm32部分代码

1.UART 通讯部分函数


 


前言

本人有幸参加活动2023年10月的“联通杯”江苏省工程实践与创新能力大赛,即中国大学生工训赛江苏省省赛。我组在初赛时十中八(识别成功率100%,但是由于我们在机械结构方面考虑不佳导致垃圾卡住),最后获得江苏省一等奖。(以下代码仅用于分享,不做商业用途)

硬件设备:

树莓派4B+8G(用于视觉识别和控制系统总控)

stm32f103c8t6(用于满载和人体红外检测)

淘晶池串口屏(x5七寸电阻触摸款)

(本文模板参考csdn关于工训赛的多个相关文档,感谢多位博主的无私分享,使我组的垃圾桶在构建之初可以集众家之所长。但碍于网络上并没有关于此竞赛的完整搭建方案,故作此篇,仅作分享。)

附照片佐证:

bad4489a80fb4f3d9685cfa60d28f0c8.jpeg

5af7de87538b431b96f92a290edf4b55.png084f5d3657b44b9e9b6b808224181068.png

 

一、机械结构设计

  1. 我组在讨论中为了防止投放垂直高度过高的问题,采取了增加斜坡的方式来减小下落速度,但是由于投放垃圾中存在扁平的碎砖片,表面过于粗糙,导致在斜坡上无法正常滑下导致在初赛中发生了垃圾卡在投放的问题。虽然我们为这个比赛准备时间很长,但是真正实践动手时间较短(大概20多天),很多实际问题只有在真正做出来后才能发现不足,加之我们只有一个机械方面学长参加(大爱学长,为了比赛承担了很多责任),导致我们在机械结构方面只是达到了最基础的功能(还有一个很重要的原因是我们都是控制小白,实现最基本的功能已经耗费我们太多时间了)

 

附机械部分图纸ede2e9175ae44364b2463e45689b1faf.png

430a95438fa941e69582c428802c0848.pngfbdf0d56a81e467daf26d8376b254b82.png

机械结构建议:

  1. ​有能力的还是要搞机械臂--这才是最终方案
  2. 或者多个差速传送带也是不错的选择,虽说其他省有一些裁判对暂存装置的理解不一样,但是国赛还是允许的(平心而论,个人认为传送带打破了只能有一个暂存区的设定,属于bug范畴,但是采用传送带大家实属无奈之举)

二、树莓派视觉识别

1.数据集的制作

(1)数据集处理部分

数据集的制作是识别训练模型最为关键的一点,并且在我的实际操作过程中发现有几大注意要点(一般人可不会告诉你哦)

1.一定要注意数据集的制作的多角度——以垃圾识别为例,在拍摄时一定要多角度多方位的拍摄多组照片,包括一个垃圾的360°照片和在托盘上的所有方位(位置点)照片。因为垃圾掉落下去具有不确定因素,可能在托盘停稳后落在托盘上的各个方位并且旋转成为各个角度,给目标检测带来不确定性。(为了更好的模拟识别环境,我选择在摄像头固定好位置后在树莓派上拍摄照片,以追求更好的仿真性)

以下是我在树莓派上运行的拍照程序(程序运行后将会逐帧保存拍摄照片——保存在images文件夹里,打标签时可以从树莓派里提取出来)

import cv2
from threading import Thread
import uuid
import os
import time
count = 0
def image_collect(cap):
    global count
    while True:
        success, img = cap.read()
        if success:
            file_name = str(uuid.uuid4())+'.jpg'
            cv2.imwrite(os.path.join('images',file_name),img)
            count = count+1
            print("save %d %s"%(count,file_name))
        time.sleep(0.4)
 
if __name__ == "__main__":
    
    os.makedirs("images",exist_ok=True)
    
    # 打开摄像头
    cap = cv2.VideoCapture(0)
 
    m_thread = Thread(target=image_collect, args=([cap]),daemon=True)
    
    while True:
 
        # 读取一帧图像
 
        success, img = cap.read()
 
        if not success:
 
            continue
 
        cv2.imshow("video",img)
 
        key =  cv2.waitKey(1) & 0xFF   
 
        # 按键 "q" 退出
        if key ==  ord('c'):
            m_thread.start()
            continue
        elif key ==  ord('q'):
            break
 
    cap.release()

(代码参考了csdn大佬——链接如下 基于树莓派4B的YOLOv5-Lite目标检测的移植与部署(含训练教程)_树莓派yolo-CSDN博客)

 (2)数据集的优化

自己在手动拍摄时无法做到真正的360°,并且如果全靠手动调整角度后期修改时会无形的添加工作量。为了更好的添加多角度照片,以下是我旋转已有照片的代码。

 使用前先在文件夹里创建imagesresult文件夹,然后把需要旋转的照片放在images里

from math import *
import cv2
import os
import glob
import imutils
import numpy as np


def rotate_img(img, angle):
    '''
    img   --image
    angle --rotation angle
    return--rotated img
    '''
    h, w = img.shape[:2]
    rotate_center = (w / 2, h / 2)
    # 获取旋转矩阵
    # 参数1为旋转中心点;
    # 参数2为旋转角度,正值-逆时针旋转;负值-顺时针旋转
    # 参数3为各向同性的比例因子,1.0原图,2.0变成原来的2倍,0.5变成原来的0.5倍
    M = cv2.getRotationMatrix2D(rotate_center, angle, 1.0)
    # 计算图像新边界
    new_w = int(h * np.abs(M[0, 1]) + w * np.abs(M[0, 0]))
    new_h = int(h * np.abs(M[0, 0]) + w * np.abs(M[0, 1]))    # 调整旋转矩阵以考虑平
    M[0, 2] += (new_w - w) / 2
    M[1, 2] += (new_h - h) / 2

    rotated_img = cv2.warpAffine(img, M, (new_w, new_h))
    return rotated_img


if __name__ == '__main__':

    output_dir = "result"
    image_names = glob.glob("images/e080b004-ddd9-404c-a518-a8f5671bc658.jpg")#这里改为自己需要旋转的照片名称,注意路径

    for image_name in image_names:
        image = cv2.imread(image_name, -1)

        for i in range(1, 361, 3):  # 修改步长为3(每3°旋转一次)可修改
            rotated_img1 = rotate_img(image, i)
            basename = os.path.basename(image_name)
            tag, _ = os.path.splitext(basename)
            cv2.imwrite(os.path.join(output_dir, 'qqq-%d.jpg' % i), rotated_img1)#这里可以修改自己的文件名称

2.数据集打标签建议

(1)网站推荐

官方指定的labelimg特别容易崩溃,本人深受其害,后发现makesense网站出奇好用,现推荐给大家——附链接  Make Sense

(2)问题示例

以本项目为例(垃圾识别)需要四种垃圾,标签需要转出为yolo形式,yolo中的第一个数字含义为物体的种类,如下图所示,不同数字代表不同种类。所以如果是小组四个人在打标签,一个人打一种,在配置标签种类时四种也都需要配上,并且每个人打标签的名称顺序以及名称的命名也要保持一致(0,1,2,3)

0 0.503021 0.511827 0.477341 0.261498
3 0.319119 0.725658 0.050600 0.053973

如果打错了想要修改标签数字怎么办,以下是我的代码 

import os

def modify_yolo_txt(file_path, new_value):
    with open(file_path, 'r') as file:
        lines = file.readlines()

    modified_lines = []
    for line in lines:
        parts = line.split(' ')
        parts[0] = str(new_value)
        modified_lines.append(' '.join(parts))

    with open(file_path, 'w') as file:
        file.writelines(modified_lines)

# 遍历指定目录下的所有txt文件
directory = 'C:/Users/under/Desktop/Hazardous/labels/val'#修改的标签文件夹(注意绝对路径)
new_value = 1 #修改想要的数字

for filename in os.listdir(directory):
    if filename.endswith('.txt'):
        file_path = os.path.join(directory, filename)
        modify_yolo_txt(file_path, new_value)

3.树莓派环境搭建部分

本项目识别采用Yolov5-lite算法,YOLOv5-Lite 与 YOLOv5 相比虽然牺牲了部分网络模型精度,但是缺极大的提升了模型的推理速度,该模型属性将更适合实战部署使用,并且使用onnxruntime替代pytorch,其运行速度更一步提升。

(1)  YOLOv5-Lite环境的部署

requirements.txt请在文章末尾资源中下载

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

4.具体Yolov5-lite算法部分

(1)引入库部分

#!/usr/bin/python
import serial
import time
import math
import smbus
import cv2
import numpy as np
import onnxruntime as ort
import random

(2)视觉识别主要部分代码

if __name__=='__main__':
    
    model_pb_path = "......"  # 这里改为自己的onnx模型,注意用yolov5-lite中的export.py导出
    so = ort.SessionOptions()
    net = ort.InferenceSession(model_pb_path, so)
    
    dic_labels = {
        0:'RecyclableWaste',
        1:'Hazardous',
        2:'kitchengarbage',
        3:'Other waste'
    }
    
    model_h = 320
    model_w = 320
    nl = 3
    na = 3
    stride = [8., 16., 32.]
    anchors = [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]]
    anchor_grid = np.asarray(anchors, dtype=np.float32).reshape(nl, -1, 2)
    
    video = 0
    cap = cv2.VideoCapture(video)
    flag_det = True
    last_detect = time.time()
    while True:

        success, img0 = cap.read()

        if success:
            
            if flag_det and time.time() - last_detect > 1.0:
                img = cv2.resize(img0, [model_w, model_h], interpolation=cv2.INTER_AREA)
                img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                img = img.astype(np.float32) / 255.0
                blob = np.expand_dims(np.transpose(img, (2, 0, 1)), axis=0)
                outs = net.run(None, {net.get_inputs()[0].name: blob})[0].squeeze(axis=0)
                outs = cal_outputs(outs, nl, na, model_w, model_h, anchor_grid, stride)
                img_h, img_w, _ = np.shape(img0)
                boxes, confs, ids = post_process_opencv(outs, model_h, model_w, img_h, img_w, thred_nms=0.4, thred_cond=0.5)
                #FPS
                t1 = time.time()
                det_boxes, scores, ids = infer_img(img0, net, model_h, model_w, nl, na, stride, anchor_grid, thred_nms=0.4, thred_cond=0.5)
                t2 = time.time()
                receive_and_send()
                for box, score, id in zip(det_boxes, scores, ids):
                    label = '%s:%.2f' % (dic_labels[id], score)
                    plot_one_box(box.astype(np.int16), img0, color=(255, 0, 0), label=label, line_thickness=None)

 (代码参考了csdn大佬——链接如下 基于树莓派4B的YOLOv5-Lite目标检测的移植与部署(含训练教程)_树莓派yolo-CSDN博客)

(3)树莓派控制舵机部分代码

class PCA9685:
    # Registers/etc.
    __SUBADR1 = 0x02
    __SUBADR2 = 0x03
    __SUBADR3 = 0x04
    __MODE1 = 0x00
    __PRESCALE = 0xFE
    __LED0_ON_L = 0x06
    __LED0_ON_H = 0x07
    __LED0_OFF_L = 0x08
    __LED0_OFF_H = 0x09
    __ALLLED_ON_L = 0xFA
    __ALLLED_ON_H = 0xFB
    __ALLLED_OFF_L = 0xFC
    __ALLLED_OFF_H = 0xFD

    def __init__(self, address=0x40, debug=False):
        self.bus = smbus.SMBus(1)
        self.address = address
        self.debug = debug
        if (self.debug):
            print("Reseting PCA9685")
        self.write(self.__MODE1, 0x00)

    def write(self, reg, value):
        "Writes an 8-bit value to the specified register/address"
        self.bus.write_byte_data(self.address, reg, value)
        if (self.debug):
            print("I2C: Write 0x%02X to register 0x%02X" % (value, reg))

    def read(self, reg):
        "Read an unsigned byte from the I2C device"
        result = self.bus.read_byte_data(self.address, reg)
        if (self.debug):
            print("I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % (self.address, result & 0xFF, reg))
        return result

    def setPWMFreq(self, freq):
        "Sets the PWM frequency"
        prescaleval = 25000000.0    # 25MHz
        prescaleval /= 4096.0       # 12-bit
        prescaleval /= float(freq)
        prescaleval -= 1.0
        if (self.debug):
            print("Setting PWM frequency to %d Hz" % freq)
            print("Estimated pre-scale: %d" % prescaleval)
        prescale = math.floor(prescaleval + 0.5)
        if (self.debug):
            print("Final pre-scale: %d" % prescale)

        oldmode = self.read(self.__MODE1);
        newmode = (oldmode & 0x7F) | 0x10        # sleep
        self.write(self.__MODE1, newmode)        # go to sleep
        self.write(self.__PRESCALE, int(math.floor(prescale)))
        self.write(self.__MODE1, oldmode)
        time.sleep(0.005)
        self.write(self.__MODE1, oldmode | 0x80)

    def setPWM(self, channel, on, off):
        "Sets a single PWM channel"
        self.write(self.__LED0_ON_L + 4 * channel, on & 0xFF)
        self.write(self.__LED0_ON_H + 4 * channel, on >> 8)
        self.write(self.__LED0_OFF_L + 4 * channel, off & 0xFF)
        self.write(self.__LED0_OFF_H + 4 * channel, off >> 8)
        if (self.debug):
            print("channel: %d  LED_ON: %d LED_OFF: %d" % (channel, on, off))

    def setServoPulse(self, channel, pulse):
        "Sets the Servo Pulse,The PWM frequency must be 50HZ"
        pulse = pulse * 4096 / 20000  # PWM frequency is 50HZ,the period is 20000us
        # 调整舵机旋转速度更快
        pulse = pulse * 1
        self.setPWM(channel, 0, int(pulse))

三、淘晶池串口屏通讯代码

        # 根据接收到的数据发送对应的控制指令
            if value == 1:
            # 向串口屏发送控制指令
                send_command("click b5,1")
                send_end()
            elif value == 2:
                # 向串口屏发送控制指令
                send_command("click b6,1")
                send_end()
            elif value == 3:
                # 向串口屏发送控制指令
                send_command("click b7,1")
                send_end()
            elif value == 4:
                # 向串口屏发送控制指令
                send_command("click b8,1")
                send_end()
            elif value == 5:
                # 向串口屏发送控制指令
                send_command("click b0,1")
                send_end()
            else:
                print("Invalid value: ", value)  # 处理无效的值
        except ValueError:
            print("Invalid data format")

 

四、stm32部分代码

1.UART 通讯部分函数

树莓派与stm32通讯我们采用的是树莓派4B与 STM32 的 UART 通讯 ,此种方法我们认为较为简单,代码比较容易

ser32 = serial.Serial('/dev/ttyAMA0', 9600, timeout=1)
# 解码从STM32接收到的数据
def decode_data(data):
    decoded_data = data.decode().strip()  # 使用ASCII解码并移除首尾空白字符
    return decoded_data
def receive_and_send():

    # 从STM32接收数据
    data = ser32.read(1)

    # 解码接收到的数据
    decoded_data = decode_data(data)

# 如果接收到的数据不为空
    if decoded_data:
    # 尝试将数据转换为整数
        try:
            value = int(decoded_data)

代码包含——yolov5lite环境部署,视觉识别,stm32部分所有代码,淘晶池串口屏打包代码及UI界面,舵机控制代码以及所有代码整合。

数据集共四千多张近五千张(已标注)

本项目所有代码均已开源,需要的可以私聊 可提供垃圾桶搭建前后相关建议

详情+QQ  3037034536

 

物联沃分享整理
物联沃-IOTWORD物联网 » 树莓派和STM32智能垃圾桶分类全流程代码分享

发表评论