基于opencv的厨余垃圾破袋分类收集系统
开始前先啰嗦一下!!!
为什么创作?
答:
在垃圾分类背景下,许多居民都能做到在家中进行厨余垃圾分袋、分类收集,但在最后投放一步却打消了许多居民的积极性,原因是居民在投放厨余垃圾时还得将塑料袋和厨余垃圾分离,其流程相对麻烦且不卫生。居民投放厨余垃圾时,需先将厨余垃圾从袋子倒进厨余垃圾桶,再把塑料袋扔到另一个垃圾桶中,这过程不仅容易脏手,还常常造成回收点周边滴漏多、臭味大等问题。在此背景下,我们期望通过计算机视觉和自动化等技术助力厨余垃圾分类投放的最后一环节,实现无接触投放、自动破袋分类。
进入正题!!!
基于什么开发?
答:
系统使用的硬件有:
- 旭日X3派开发板
- MEGA 2560开发板
- 摄像头
- 步进电机
- A4988步进电机驱动模块
- 超声波
- 舵机6个
系统使用的软件有:
- ubuntu20
- Ardunio IDE
- PyCharm
- opencv
第一次使用Linux平台进行裸机开发,之前一直使用普通的单片机,旭日X3派裸机开发和树莓派基本差不多,根据官方文档,上手非常简单。旭日X3派主要是用来跑opencv的图像处理,对图像处理后对步进电机进行相应的控制,旭日X3派通过I2C通信和MEGA 2560开发板进行数据交互,MEGA 2560再去接收传感器数据和控制舵机等。这里为什么不直接用X3派来控制舵机和采集传感器数据呢?X3派也有硬件pwm信号,但是其频率太高,如果模拟pwm,在Linux中并不太理想,还有个原因是需要一直给屏幕输出画面,因此使用了多进程,但我发现使用了多线程后,步进电机的控制并不太顺,感觉会出现丢步,所以这种情况更不用说去控制别的了,说不定会出其他问题。以上这些问题还没有找到很好的解决方法,反正多线程是不行的了。
基本的实现过程:
第一次使用了opencv,只是使用了简单的图像处理
将背景布置为白色的前提下,使用OpenCV从摄像头拉取视频流,设置图像分辨率为1280x720,逐帧读取图像,将原始 BGR 彩色图像转换为灰度图像;对灰度图像进行二值化处理,颜色较深的区域设为白色,其余设为黑色,将目标物体从背景中分离出来,以便于后续的轮廓分析图像;执行形态学变换操作,消除二值化图像中的孔洞和噪点,增强物体轮廓的连通性;通过轮廓检测检测[2]图像中的边缘轮廓,并遍历计算轮廓面积,根据设定的阈值过滤掉面积过小的轮廓,从而找到物体的边缘;进行多边形逼近,如果逼近后的多边形有4个顶点,且类似塑料袋的形状,则可确定该物体为目标物体,绘制矩形框以标记目标物体,最后使用numpy库遍历目标物体轮廓计算出塑料袋轮廓最左边点(leftmost)、最右边点(rightmost)和最底边点(bottommost)的坐标并保存,为后续检测位置和识别破袋状况提供数据基础。
在破袋系统中采集的图像,元素较单一,塑料袋占据了大部分画面,而且不同规格的塑料袋形状差异小,宽度由上往下呈增加趋势,因此在视觉检测塑料袋位置大小和识别破袋状况算法上巧妙利用了以上特征[3]。前期图像处理已标记到了目标物体(塑料袋),并获得塑料袋轮廓相关点坐标。
通过计算left_most[X]与right_most[X]的差与bottom_most[Y]的值可以得知塑料袋相对宽度和相对长度,进而用于判断塑料袋大小:
计算left_most[X]和ight_most[X]的中点确定水平位置,bottom_most[Y]确定垂直位置:
通过判断在不同时间点相对大小的值的变化与形状的变化识别塑料袋内的厨余垃圾是否分离:
图像处理实现的代码:
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -*-coding:gb2312-*
import cv2
import time
import os
import numpy as np
import global_val
# 摄像头是否打开
camera_open = False
# 摄像头开关
camera_state = True
# 获取当前时间的字符串形式
def get_current_time_str():
return time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time()))
# 拍照并保存照片
def capture_photo(cap):
# 获取一帧图像
ret, frame1 = cap.read()
# 调整图像尺寸
frame1 = cv2.resize(frame1, PHOTO_SIZE)
# 保存照片
# file_name = f'{get_current_time_str()}.jpg'
file_name = f'11.jpg'
file_path = os.path.join(global_val.SAVE_PATH, file_name)
cv2.imwrite(file_path, frame1)
def shibie(path, a):
# 读取图像
if a:
img = cv2.imread(path)
else:
img = path
# 将图像转为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 对灰度图像进行二值化处理,将颜色较深的区域设为白色,其余设为黑色
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# 找到轮廓
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 找到最大的轮廓
max_contour = max(contours, key=cv2.contourArea)
# 计算最大轮廓的外接矩形
x, y, w, h = cv2.boundingRect(max_contour)
area = cv2.contourArea(max_contour)
# 仅对面积较大的轮廓进行处理,过滤掉一些噪声
if area > 10000 and h < 720 and w < 1280:
# 在原图上绘制矩形框
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 计算最左边、最右边和最底边的点的坐标
leftmost = tuple(max_contour[max_contour[:, :, 0].argmin()][0])
rightmost = tuple(max_contour[max_contour[:, :, 0].argmax()][0])
bottommost = tuple(max_contour[max_contour[:, :, 1].argmax()][0])
global_val.leftmost = leftmost
global_val.rightmost = rightmost
global_val.bottommost = bottommost
# 在最左边、最右边和最底边的点的范围画一条水平线
cv2.line(img, (leftmost[0], leftmost[1]), (rightmost[0], rightmost[1]), (0, 0, 255), 2)
cv2.line(img, (bottommost[0], bottommost[1]), (leftmost[0], leftmost[1]), (0, 0, 255), 2)
cv2.line(img, (bottommost[0], bottommost[1]), (rightmost[0], rightmost[1]), (0, 0, 255), 2)
if __name__ == '__main__':
print('yes')
旭日X3派的IO操作的实现过程
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -*-coding:gb2312-*
import Hobot.GPIO as GPIO
import time
dirPin_X = 16
stepPin_X = 18
STEPS_PER_REV = 200
dirPin_Y = 22
stepPin_Y = 24
limit_pin_XY = 32
chufa_Pin_1 = 29
chufa_Pin_2 = 31
limit_X = 1
limit_Y = 0
# 步进电机运动方向
R_D = 0
L_U = 1
# 红外触发信号
gpio_1 = False
gpio_2 = False
def delayMicroseconds(t):
t = t / 1000000
delay_mark = time.time()
while True:
offset = time.time() - delay_mark
if offset >= t:
# print(offset * 1000000)
# print(delay_mark * 1000000)
break
def IO_begin():
GPIO.setmode(GPIO.BOARD) # BOARD pin-numbering scheme
GPIO.setup(limit_pin_XY, GPIO.IN)
GPIO.setup(chufa_Pin_1, GPIO.IN)
GPIO.setup(chufa_Pin_2, GPIO.IN)
GPIO.setup(dirPin_Y, GPIO.OUT)
GPIO.setup(dirPin_X, GPIO.OUT)
GPIO.setup(stepPin_X, GPIO.OUT)
GPIO.setup(stepPin_Y, GPIO.OUT)
# X轴low为往右
# Y轴low为往下
def ChuFa(SIGNAL):
global gpio_1
global gpio_2
while True:
value = GPIO.input(chufa_Pin_1)
gpio_1 = '1'
if value == GPIO.LOW or gpio_1 == "1":
SIGNAL['qian_HW'] = True
print('', SIGNAL['qian_HW'])
break
while True:
value = GPIO.input(chufa_Pin_1)
gpio_2 = '1'
if value == GPIO.LOW or gpio_2 == "1":
# SIGNAL['hou_HW'] = True
# print('', SIGNAL['qian_HW'])
break
while True:
value = GPIO.input(limit_pin_XY)
if value == GPIO.LOW:
SIGNAL['hou_HW'] = True
print('', SIGNAL['qian_HW'])
print('ChuFa')
break
def Mov_begin():
while True:
value = GPIO.input(limit_pin_XY)
# value = input('input:')
Mov_X(0.1, R_D)
if value == GPIO.LOW:
Mov_X(6, L_U)
print('11111_Mov_begin()_11111')
break
while True:
value = GPIO.input(limit_pin_XY)
# value = input('input:')
Mov_Y(0.1, L_U)
if value == GPIO.LOW:
Mov_Y(16, R_D)
break
return True
def Mov_Y(st, Pin_dir):
v = 1000
if Pin_dir == 1:
GPIO.output(dirPin_Y, GPIO.LOW)
if Pin_dir == 0:
GPIO.output(dirPin_Y, GPIO.HIGH)
for i in range(int(st * 50)):
GPIO.output(stepPin_Y, GPIO.HIGH)
delayMicroseconds(v)
GPIO.output(stepPin_Y, GPIO.LOW)
delayMicroseconds(v)
def Mov_Y_1(st, v, Pin_dir):
if Pin_dir == 1:
GPIO.output(dirPin_Y, GPIO.LOW)
if Pin_dir == 0:
GPIO.output(dirPin_Y, GPIO.HIGH)
for i in range(int(st * 50)):
GPIO.output(stepPin_Y, GPIO.HIGH)
delayMicroseconds(v)
GPIO.output(stepPin_Y, GPIO.LOW)
delayMicroseconds(v)
# st为距离,单位为厘米;Pin为步进电机引脚;v为运行速度
def Mov_X(st, Pin_dir):
if st < 20:
v = 1000
if Pin_dir == 0:
GPIO.output(dirPin_X, GPIO.LOW)
if Pin_dir == 1:
GPIO.output(dirPin_X, GPIO.HIGH)
for i in range(int(st * 50)):
GPIO.output(stepPin_X, GPIO.HIGH)
delayMicroseconds(v)
GPIO.output(stepPin_X, GPIO.LOW)
delayMicroseconds(v)
else:
v = 1500
v1 = 500
s = st / 7 * 50
if Pin_dir == 0:
GPIO.output(dirPin_X, GPIO.LOW)
if Pin_dir == 1:
GPIO.output(dirPin_X, GPIO.HIGH)
for i in range(int(s)):
GPIO.output(stepPin_X, GPIO.HIGH)
delayMicroseconds(v)
GPIO.output(stepPin_X, GPIO.LOW)
delayMicroseconds(v)
v = v - (v - v1) / s
s = st / 7 * 4 * 50
for i in range(int(s)):
GPIO.output(stepPin_X, GPIO.HIGH)
delayMicroseconds(v1)
GPIO.output(stepPin_X, GPIO.LOW)
delayMicroseconds(v1)
s = st / 7 * 2 * 50
for i in range(int(s)):
GPIO.output(stepPin_X, GPIO.HIGH)
delayMicroseconds(v)
GPIO.output(stepPin_X, GPIO.LOW)
delayMicroseconds(v)
v = v + (v - v1) * 3 / s / 2
if __name__ == '__main__':
GPIO.cleanup()
print('6666')
IO_begin()
time.sleep(2)
for i in range(4):
GPIO.output(dirPin_X, GPIO.HIGH)
for i in range(200):
GPIO.output(stepPin_X, GPIO.HIGH)
delayMicroseconds(1000);
GPIO.output(stepPin_X, GPIO.LOW)
delayMicroseconds(1000);
print('end6666')
time.sleep(2)
print('77777')
Mov_begin()
print('88888')
主函数代码,包含了基本的控制逻辑:
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -*-coding:gb2312-*
import threading
import cv2
import time
import os
import PoDai
import ShiBie
import global_val
import I2C
import IO
# 摄像头编号
# 照片保存路径
SAVE_PATH = 'cam'
# 照片尺寸
PHOTO_SIZE = (600, 400)
# 识别阈值
CONFIDENCE_THRESHOLD = 0.5
# 摄像头是否打开
camera_open = False
# 摄像头开关
camera_state = True
# 步进电机运动方向
R_D = 0
L_U = 1
def Setcamera(cap):
cap.set(6, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))
cap.set(3, 480)
cap.set(4, 640)
def CatchUsbVideo(window_name, id):
cv2.namedWindow(window_name) # 写入打开时视频框的名称
# 捕捉摄像头
cap = cv2.VideoCapture(id) # camera_idx 的参数是0代表是打开笔记本的内置摄像头,也可以写上自己录制的视频路径
Setcamera(cap)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
# cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
# event = threading.Event()
while cap.isOpened(): # 判断摄像头是否打开,打开的话就是返回的是True
# 读取图像
ok, frame = cap.read() # 读取一帧图像,该方法返回两个参数,ok true 成功 false失败,frame一帧的图像,是个三维矩阵,当输入的是一个是视频文件,读完ok==flase
if not ok: # 如果读取帧数不是正确的则ok就是False则该语句就会执行
break
#将图像转为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 对灰度图像进行二值化处理,将颜色较深的区域设为白色,其余设为黑色
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# 找到轮廓
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 找到最大的轮廓
max_contour = max(contours, key=cv2.contourArea)
# 计算最大轮廓的外接矩形
x, y, w, h = cv2.boundingRect(max_contour)
area = cv2.contourArea(max_contour)
# 仅对面积较大的轮廓进行处理,过滤掉一些噪声
if area > 10000 and h < 720 and w < 1280:
# 在原图上绘制矩形框
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 计算最左边、最右边和最底边的点的坐标
leftmost = tuple(max_contour[max_contour[:, :, 0].argmin()][0])
rightmost = tuple(max_contour[max_contour[:, :, 0].argmax()][0])
bottommost = tuple(max_contour[max_contour[:, :, 1].argmax()][0])
global_val.leftmost = leftmost
global_val.rightmost = rightmost
global_val.bottommost = bottommost
# 在最左边、最右边和最底边的点的范围画一条水平线
cv2.line(img, (leftmost[0], leftmost[1]), (rightmost[0], rightmost[1]), (0, 0, 255), 2)
cv2.line(img, (bottommost[0], bottommost[1]), (leftmost[0], leftmost[1]), (0, 0, 255), 2)
cv2.line(img, (bottommost[0], bottommost[1]), (rightmost[0], rightmost[1]), (0, 0, 255), 2)
# 获取一帧识别
# threading.Event()
# if event.is_set():
# ShiBie.capture_photo(cap)
# 显示图像
cv2.imshow(window_name, frame) # 显示视频到窗口
c = cv2.waitKey(10)
if c & 0xFF == ord('q'): # 键盘按q退出视频
break
cap.release() # 释放摄像头
cv2.destroyAllWindows() # 销毁所有窗口
def podai_doudong(a):
for i in range(a):
IO.Mov_Y_1(6, 600, L_U)
IO.Mov_Y_1(6, 600, R_D)
def print_cam(a):
if a == 'l':
print("Leftmost point:", global_val.leftmost)
elif a == 'r':
print("rightmost point:", global_val.rightmost)
elif a == 'b':
print("Bottommost point:", global_val.bottommost)
else:
print("1Leftmost point:", global_val.leftmost)
print("1Rightmost point:", global_val.rightmost)
print("1Bottommost point:", global_val.bottommost)
if __name__ == '__main__':
thread_CV = threading.Thread(target=CatchUsbVideo, kwargs={"window_name": 'cam', "id": global_val.CAMERA_ID})
thread_CV.start()
IO.IO_begin()
while True:
high_Y = 0
delay_mark = 0
# 电机初始化
IO.Mov_begin()
IO.ChuFa(global_val.HW_SIGNAL)
time.sleep(0.5)
if global_val.HW_SIGNAL['qian_HW'] and global_val.HW_SIGNAL['hou_HW']:
print("11111_zhun bei_11111")
# 舵机限位开,I2C.send(10)为关
I2C.send(1)
time.sleep(2)
IO.Mov_X(20, L_U)
# global_val.Img_SIGNAL_in = True
time.sleep(0.5)
while True:
# 判断底部位置是否正确。屏幕高为720,如果底部y坐标大于700,说明位置不对
if global_val.bottommost[1] == 0 or global_val.bottommost[1] > 700:
print("22222_shangxia tiaozheng_111111")
print_cam('b')
if high_Y != 123:
IO.Mov_Y(1, L_U)
high_Y = high_Y + 1
time.sleep(0.5)
if high_Y > 10:
print_cam('b')
high_Y = 123
print("22222_SX_tiaozheng_shibai_outtime_222222")
break
# 精确调整
if 500 < global_val.bottommost[1] < 600:
print_cam('b')
IO.Mov_Y(0.5, R_D)
print("222222_SangXia_jingque_tiaozheng_333333")
# 正确位置
if 600 < global_val.bottommost[1] < 700:
print_cam('b')
print("2222222_shangxia_tiaozheng_OK_444444")
break
# 上下调整失败,重来
if high_Y == 123:
print("333333_tiaozheng_not_OK_continue_11111")
continue
while True:
if global_val.rightmost[0] < 1280:
print("44444444_panduang_kuangdu_11111")
print_cam('l')
print_cam('r')
# 判断宽度
Wight_1 = global_val.rightmost[0] - global_val.leftmost[0]
print('print(Wight_1):', Wight_1)
time.sleep(2)
Wight_2 = global_val.rightmost[0] - global_val.leftmost[0]
print('print(Wight_2):', Wight_2)
print_cam('l')
print_cam('r')
Wight = Wight_2 - Wight_1
print('print(Wight)_1:', Wight)
# 采集两次宽度,如果两次宽度都大于限定值,且两次的差小于另一限定值
if Wight_1 > 200 and Wight_2 > 200 and -100 < Wight < 100:
Wight = (Wight_2 + Wight_1) / 2
print('print(Wight)_2:', Wight)
print("44444444_end")
break
else:
IO.Mov_X(0.5, L_U)
# 破袋环节
print("5555555_podai_11111")
# I2C.send(2)为执行破袋
I2C.send(2)
while True:
podai_stat = I2C.read()
if podai_stat == 20:
print("5555555_podai_end_22222")
podai_stat = 0
break
podai_doudong(3)
# 第二轮破袋,向右移动6厘米
IO.Mov_X(6, R_D)
I2C.send(2)
while True:
podai_stat = I2C.read()
if podai_stat == 20:
print("5555555_podai_end_333333")
podai_stat = 0
break
podai_doudong(3)
# 破袋判断
time.sleep(2)
podai_timeout = 0
while True:
# 获取破袋后的宽度
Wight_end = global_val.rightmost[0] - global_val.leftmost[0]
print_cam('b')
print_cam('l')
print_cam('r')
# 判断条件
if Wight / Wight_end >= 1.8 or podai_timeout > 2:
# 分类
IO.Mov_X(36, L_U)
# I2C.send(3)执行脱袋
I2C.send(3)
podai_doudong(3)
# 挂钩修正
I2C.send(30)
# 限位复原
I2C.send(10)
print("5555555_podai_end_555555")
break
else:
print("5555555_podai_22222")
podai_timeout = podai_timeout + 1
# I2C.send(2)为执行破袋
I2C.send(2)
while True:
podai_stat = I2C.read()
if podai_stat == 20:
print("5555555_podai_end_44444")
podai_stat = 0
break
podai_doudong(3)
continue
整体项目代码写得不太规范,逻辑可能也不太好