1.前言
智能送药小车是我在备战2023年电赛时做的训练题。通过研究,我发现网上大多数方案将巡线与数字识别分开实现。然而,我的想法是将这两者结合在一个OpenMV系统中,来模拟在比赛期间缺少了灰度传感器(因为巡线是循红线)的解决方案。(我是使用STC32作为小车的主控来控制其电机的运动,通信就是STC32和openmv的,实际上将串口接收和发送处理好所有单片机都可以使用这个方案)
(训练题目比较简陋“擦汗emoji”)
2.题目任务
设计并制作一款智能送药小车,模拟完成在医院药房与病房间药品的送取作业。以下是院区结构的示意图(图1),走廊两侧的墙体用黑色实线表示,走廊地面上有一条居中的红色实线,并且放置了标识病房号的黑色数字纸张。药房和近端病房号(1、2号)位置固定不变,而中部病房和远端病房号(3-8号)在测试时会随机设定。
工作过程
-
初始状态:
- 参赛者将小车手动摆放在药房处,使车头的投影位于门口区域内,并使车头面向病房。
-
药品装载:
- 手持数字标号纸张,由小车识别病房号。小车识别后,将约200克药品一次性装载到送药小车上。
-
自动送药:
- 小车在检测到药品装载完成后,自动开始运送。
- 小车根据走廊上的标识信息自动识别病房号,并沿着标识路径寻径,将药品送到指定的病房(车头的投影位于门口区域内)。
-
卸药操作:
- 在病房处,点亮红色指示灯以指示药品送达。
- 病房人员人工卸载药品后,小车会自动熄灭红色指示灯,并开始返回药房。
-
返回药房:
- 小车在返回药房时,确保车头的投影位于门口区域内并面向药房。
- 返回到药房后,小车点亮绿色指示灯,表示药品送取任务完成
3.实现原理
手持数字标号纸张,由小车识别病房号。小车识别后,将约200克药品一次性装载到送药小车上。然后去找到这个指定的数字病房(就好像是一只小狗,我扔出去一个球,然后跑过去捡回来,哈哈哈!!!)。言归正传,我是通过根据不同的任务模式(Find_Task
),执行相应的图像处理任务,例如目标模板的匹配、目标病房的寻找。
-
图像处理与目标检测:
- 通过摄像头获取图像。
- 识别和处理图像中的特定颜色块和模板。
- 根据识别的结果计算偏转角度,控制系统的运动方向。
- 检测十字路口并发送信号。
-
串口通信:
- 通过串口与其他设备进行数据传输,例如发送转向角度或其他控制指令。
-
任务模式处理:
- 根据不同的任务模式(
Find_Task
),执行相应的图像处理任务,例如目标模板的匹配。
- 根据不同的任务模式(
-
巡线功能:
- 实现对图像中预定义路径的跟踪。
主要功能模块
-
UartReceiveDate()
函数:- 处理串口数据接收。
- 解析串口接收到的数据,并根据数据执行不同的操作(如检测十字路口)。
-
FirstFindTemplate(template)
和FindTemplate(template)
函数:- 这些函数用于查找特定的模板图像并返回匹配结果。
- 模板匹配是图像识别中的一种常见技术,通过对比预定义的模板图像与当前图像中的区域来确定匹配程度。
-
xunji()
函数:- 执行巡线功能,检测图像中的颜色块、计算偏转角度,并发送控制信号以调整方向。
ROI
(Region of Interest)区域的定义用于限制搜索范围,提升检测效率。
-
FirstFindedNum()
函数:- 识别手持的数字标号纸张,上传病房号到小车主控。
5.FindedNum()
函数:
1.在小车运动的过程中,不断的寻找目标病房。
以下是具体代码的实现:(模板图像需要自己进行拍摄数字卡片)
import sensor, image, time, math , ustruct ,pyb # 导入所需模块
from image import SEARCH_EX, SEARCH_DS # 从image模块引入SEARCH_EX和SEARCH_DS,用于图像模板匹配
from pyb import UART, Pin, Timer, LED # 从pyb模块导入UART、Pin、Timer和LED,控制串口、引脚、定时器和LED
import json # 导入json模块,用于处理JSON数据
# 定义灰度阈值,用于图像处理
GRAYSCALE_THRESHOLD = [(36, 139)]
THRESHOLD = (17, 44, 12, 127, 0, 127) # 定义颜色阈值
sensor.reset() # 重置摄像头传感器
# 设置摄像头传感器的参数
sensor.set_contrast(1) # 设置对比度为1
sensor.set_gainceiling(16) # 设置增益上限为16
sensor.set_framesize(sensor.QQVGA) # 设置图像分辨率为QQVGA(160x120)
sensor.set_pixformat(sensor.GRAYSCALE) # 设置图像为灰度模式
sensor.set_windowing(0, 30, 300, 160) # 设置观察窗口为(0, 30, 300, 160)
ROIS = [ # 定义ROI区域及其权重
(0, 100, 160, 20, 0.7), # ROI区域1,权重0.7
(0, 50, 160, 20, 0.3), # ROI区域2,权重0.3
(0, 0, 160, 20, 0.1) # ROI区域3,权重0.1
]
# 加载模板图像
template01 = image.Image("/1.pgm")
template02 = image.Image("/2.pgm")
template03 = image.Image("/3.pgm")
template04 = image.Image("/4.pgm")
template05 = image.Image("/5.pgm")
template06 = image.Image("/6.pgm")
template07 = image.Image("/7.pgm")
template08 = image.Image("/8.pgm")
# 加载不同方位的模板图像
template3L = image.Image("/3l.pgm")
template3LL = image.Image("/3ll.pgm")
template3R = image.Image("/3r.pgm")
template3RR = image.Image("/3rr.pgm")
template4L = image.Image("/4l.pgm")
template4LL = image.Image("/4ll.pgm")
template4R = image.Image("/4r.pgm")
template4RR = image.Image("/4rr.pgm")
template5L = image.Image("/5l.pgm")
template5LL = image.Image("/5ll.pgm")
template5R = image.Image("/5r.pgm")
template5RR = image.Image("/5rr.pgm")
template6L = image.Image("/6l.pgm")
template6LL = image.Image("/6ll.pgm")
template6R = image.Image("/6r.pgm")
template6RR = image.Image("/6rr.pgm")
template7L = image.Image("/7l.pgm")
template7LL = image.Image("/7ll.pgm")
template7R = image.Image("/7r.pgm")
template7RR = image.Image("/7rr.pgm")
template8L = image.Image("/8l.pgm")
template8LL = image.Image("/8ll.pgm")
template8R = image.Image("/8r.pgm")
template8RR = image.Image("/8rr.pgm")
templates = ["/tl1.pgm", "/tl2.pgm", "/tr1.pgm", "/tr2.pgm", "/tt1.pgm", "/tt2.pgm", "/tt3.pgm"]
uart = pyb.UART(3, 115200, timeout_char=1000) # 定义串口3,波特率115200,字符超时1000毫秒
blue_led = LED(3) # 初始化蓝色LED
# 通过接收STM32F103传来的数据来决定使用哪种模式
# FindTask:1. 轮询1~8,直至识别到;2. 根据F103给的值,单纯识别那个数
Find_Task = 1 # 初始化任务模式为1
Target_Num = 0 # 初始化目标数字为0
find_flag = 0 # 初始化查找标志
data = [0x00]*9 # 初始化数据缓存
# 串口接收数据函数处理
def UartReceiveDate():
global Find_Task
global Target_Num
global x
global data
data[2] = uart.readchar() # 读取串口数据
time.sleep_ms(500) # 延时500毫秒
if data[2] >= 48 and data[2] <= 57: # 检查数据是否为数字(ASCII范围48-57)
Find_Task = 2 # 设置任务模式为2
Target_Num = data[2] - 48 # 将ASCII字符转换为数字
print("模式:", Find_Task, "目标病房;", Target_Num) # 打印当前模式和目标病房号
# 当定时器回调时,将返回定时器对象
def tick(timer):
blue_led.toggle() # 切换LED状态
tim = Timer(2, freq=5) # 创建定时器2,频率5Hz
tim.callback(tick) # 设置定时器回调函数为tick
# FindTask == 0 时使用的模板匹配函数
def FirstFindTemplate(template):
sensor.set_pixformat(sensor.GRAYSCALE) # 设置图像为灰度模式
img = sensor.snapshot() # 拍摄一张图片
R = img.find_template(template, 0.8, step=1, roi=(55, 0, 50, 90), search=SEARCH_EX) # 在指定ROI区域内进行模板匹配
return R # 返回匹配结果
def FirstFindedNum(R, Finded_Num):
global Find_Task
global find_flag
img.draw_rectangle(R, color=(225, 0, 0)) # 绘制匹配到的模板的矩形框
time.sleep_ms(100) # 延时100毫秒
find_flag = 1 # 设置查找标志为1
Num = Finded_Num # 记录找到的数字
uart.write(Finded_Num) # 通过串口发送找到的数字
print("目标病房号:", Num) # 打印目标病房号
def FindTemplate(template):
sensor.set_pixformat(sensor.GRAYSCALE) # 设置图像为灰度模式
img = sensor.snapshot() # 拍摄一张图片
R = img.find_template(template, 0.8, step=1, search=SEARCH_EX) # 在全图范围内进行模板匹配
return R # 返回匹配结果
def FindedNum(R, Finded_Num):
global Find_Task
global find_flag
img.draw_rectangle(R, color=(225, 0, 0)) # 绘制匹配到的模板的矩形框
# 根据模板的x坐标判断其位置
if R[0] >= 65:
LoR = "r" # 如果x坐标大于等于65,表示在右边
elif 0 < R[0] <= 65:
LoR = "l" # 如果x坐标在0到65之间,表示在左边
uart.write(LoR) # 通过串口发送位置标识
find_flag = 1 # 设置查找标志为1
Num = Finded_Num # 记录找到的数字
print("识别到的数字是:", Num, "此数字所在方位:", LoR) # 打印识别到的数字及其位置
def xunji():
sensor.set_pixformat(sensor.RGB565) # 设置图像为RGB565模式
img = sensor.snapshot().binary([THRESHOLD]) # 拍摄一张图片并进行二值化处理
line = img.get_regression([(100, 100)], robust=True) # 进行直线回归分析
centroid_sum = 0 # 初始化质心和
for r in ROIS: # 遍历每个ROI区域
blobs = img.find_blobs(GRAYSCALE_THRESHOLD, merge=True) # 查找图像中的颜色块
if blobs:
most_pixels = 0 # 初始化最大像素数
largest_blob = 0 # 初始化最大颜色块索引
for i in range(len(blobs)): # 遍历所有找到的颜色块
if blobs[i].pixels() > most_pixels: # 找到最大的颜色块
most_pixels = blobs[i].pixels()
largest_blob = i
img.draw_rectangle(blobs[largest_blob].rect()) # 绘制颜色块的矩形框
img.draw_cross(blobs[largest_blob].cx(), blobs[largest_blob].cy()) # 绘制颜色块的中心点
centroid_sum += blobs[largest_blob].cx() * r[4] # 计算质心和
weight_sum = 1.1 # 权重和
center_pos = (centroid_sum / weight_sum) # 计算中心位置
deflection_angle = -math.atan((center_pos - 160) / 120) # 计算偏转角度
deflection_angle = math.degrees(deflection_angle) # 转换为角度
ROI = [(10, 0, 40, 50), (110, 0, 50, 50)] # 定义两个ROI区域
blobs1 = img.find_blobs(GRAYSCALE_THRESHOLD, roi=ROI[0], area_threshold=150, merge=True) # 查找第一个ROI区域的颜色块
if blobs1:
blobs2 = img.find_blobs(GRAYSCALE_THRESHOLD, roi=ROI[1], area_threshold=150, merge=True) # 查找第二个ROI区域的颜色块
if blobs2:
print("十字路口") # 打印检测到十字路口
uart.write("+") # 通过串口发送十字路口信号
time.sleep(1) # 延时1秒
A = deflection_angle # 将偏转角度赋值给A
uart_buf = int(A) + 55 # 将角度转换为整数并加上55
uart_buf1 = uart_buf - 45 # 计算调整后的角度
print("Turn Angle: %d" % uart_buf) # 打印转向角度
print('you send:', uart_buf1) # 打印发送的数据
uart.write(str(uart_buf1)) # 通过串口发送数据
sensor.set_pixformat(sensor.GRAYSCALE) # 设置图像为灰度模式
img = sensor.snapshot() # 拍摄一张图片
clock = time.clock() # 创建一个时钟对象
while True:
clock.tick() # 更新时钟
img = sensor.snapshot() # 拍摄一张图片
UartReceiveDate() # 处理串口数据
if Find_Task == 1: # 如果任务模式为1
r01 = FirstFindTemplate(template01) # 查找模板01
r02 = FirstFindTemplate(template02) # 查找模板02
r03 = FirstFindTemplate(template03) # 查找模板03
r04 = FirstFindTemplate(template04) # 查找模板04
r05 = FirstFindTemplate(template05) # 查找模板05
r06 = FirstFindTemplate(template06) # 查找模板06
r07 = FirstFindTemplate(template07) # 查找模板07
r08 = FirstFindTemplate(template08) # 查找模板08
if r01:
FirstFindedNum(r01, 'A') # 如果找到模板01,处理结果
elif r02:
FirstFindedNum(r02, 'B') # 如果找到模板02,处理结果
elif r03:
FirstFindedNum(r03, 'C') # 如果找到模板03,处理结果
elif r04:
FirstFindedNum(r04, 'D') # 如果找到模板04,处理结果
elif r05:
FirstFindedNum(r05, 'E') # 如果找到模板05,处理结果
elif r06:
FirstFindedNum(r06, 'F') # 如果找到模板06,处理结果
elif r07:
FirstFindedNum(r07, 'G') # 如果找到模板07,处理结果
elif r08:
FirstFindedNum(r08, 'I') # 如果找到模板08,处理结果
xunji() # 执行巡线功能
elif Find_Task == 2: # 如果任务模式为2
if Target_Num == 3: # 如果目标数字为3
# 进行数字3的模板匹配
r3L = FindTemplate(template3L)
r3LL = FindTemplate(template3LL)
r3R = FindTemplate(template3R)
r3RR = FindTemplate(template3RR)
if r3L:
FindedNum(r3L, 3) # 如果找到模板3L,处理结果
elif r3LL:
FindedNum(r3LL, 3) # 如果找到模板3LL,处理结果
elif r3R:
FindedNum(r3R, 3) # 如果找到模板3R,处理结果
elif r3RR:
FindedNum(r3RR, 3) # 如果找到模板3RR,处理结果
else:
FH = bytearray([0x2C, 0x12, 0x00, 0x00, find_flag, Find_Task, 0x5B]) # 创建错误消息
uart.write(FH) # 通过串口发送错误消息
elif Target_Num == 4: # 如果目标数字为4
# 进行数字4的模板匹配
r4L = FindTemplate(template4L)
r4LL = FindTemplate(template4LL)
r4R = FindTemplate(template4R)
r4RR = FindTemplate(template4RR)
if r4L:
FindedNum(r4L, 4) # 如果找到模板4L,处理结果
elif r4LL:
FindedNum(r4LL, 4) # 如果找到模板4LL,处理结果
elif r4R:
FindedNum(r4R, 4) # 如果找到模板4R,处理结果
elif r4RR:
FindedNum(r4RR, 4) # 如果找到模板4RR,处理结果
else:
FH = bytearray([0x2C, 0x12, 0x00, 0x00, find_flag, Find_Task, 0x5B]) # 创建错误消息
uart.write(FH) # 通过串口发送错误消息
elif Target_Num == 5: # 如果目标数字为5
# 进行数字5的模板匹配
r5L = FindTemplate(template5L)
r5LL = FindTemplate(template5LL)
r5R = FindTemplate(template5R)
r5RR = FindTemplate(template5RR)
if r5L:
FindedNum(r5L, 5) # 如果找到模板5L,处理结果
elif r5LL:
FindedNum(r5LL, 5) # 如果找到模板5LL,处理结果
elif r5R:
FindedNum(r5R, 5) # 如果找到模板5R,处理结果
elif r5RR:
FindedNum(r5RR, 5) # 如果找到模板5RR,处理结果
else:
FH = bytearray([0x2C, 0x12, 0x00, 0x00, find_flag, Find_Task, 0x5B]) # 创建错误消息
uart.write(FH) # 通过串口发送错误消息
xunji() # 执行巡线功能
elif Target_Num == 6: # 如果目标数字为6
# 进行数字6的模板匹配
r6L = FindTemplate(template6L)
r6LL = FindTemplate(template6LL)
r6R = FindTemplate(template6R)
r6RR = FindTemplate(template6RR)
if r6L:
FindedNum(r6L, 6) # 如果找到模板6L,处理结果
elif r6LL:
FindedNum(r6LL, 6) # 如果找到模板6LL,处理结果
elif r6R:
FindedNum(r6R, 6) # 如果找到模板6R,处理结果
elif r6RR:
FindedNum(r6RR, 6) # 如果找到模板6RR,处理结果
else:
FH = bytearray([0x2C, 0x12, 0x00, 0x00, find_flag, Find_Task, 0x5B]) # 创建错误消息
uart.write(FH) # 通过串口发送错误消息
xunji() # 执行巡线功能
elif Target_Num == 7: # 如果目标数字为7
# 进行数字7的模板匹配
r7L = FindTemplate(template7L)
r7LL = FindTemplate(template7LL)
r7R = FindTemplate(template7R)
r7RR = FindTemplate(template7RR)
if r7L:
FindedNum(r7L, 7) # 如果找到模板7L,处理结果
elif r7LL:
FindedNum(r7LL, 7) # 如果找到模板7LL,处理结果
elif r7R:
FindedNum(r7R, 7) # 如果找到模板7R,处理结果
elif r7RR:
FindedNum(r7RR, 7) # 如果找到模板7RR,处理结果
else:
FH = bytearray([0x2C, 0x12, 0x00, 0x00, find_flag, Find_Task, 0x5B]) # 创建错误消息
uart.write(FH) # 通过串口发送错误消息
xunji() # 执行巡线功能
elif Target_Num == 8: # 如果目标数字为8
# 进行数字8的模板匹配
r8L = FindTemplate(template8L)
r8LL = FindTemplate(template8LL)
r8R = FindTemplate(template8R)
r8RR = FindTemplate(template8RR)
if r8L:
FindedNum(r8L, 8) # 如果找到模板8L,处理结果
elif r8LL:
FindedNum(r8LL, 8) # 如果找到模板8LL,处理结果
elif r8R:
FindedNum(r8R, 8) # 如果找到模板8R,处理结果
elif r8RR:
FindedNum(r8RR, 8) # 如果找到模板8RR,处理结果
else:
FH = bytearray([0x2C, 0x12, 0x00, 0x00, find_flag, Find_Task, 0x5B]) # 创建错误消息
uart.write(FH) # 通过串口发送错误消息
xunji() # 执行巡线功能
else:
time.sleep_ms(100) # 如果没有找到目标,延时100毫秒
总结
核心功能是图像识别与处理,通过识别目标物体、计算运动方向、以及处理不同的任务模式来控制系统。结合串口通信,代码实现了图像处理与外部设备的交互,同时具备了处理错误情况的能力。这种系统通常用于机器人、自动驾驶等应用场景中,能够根据环境信息进行实时的决策和控制。