使用兩塊OpenMV解答送藥小車視覺部分
前言:
最近參加了2021年電賽的F題,因?yàn)橹T多原因未能完賽,現(xiàn)將圖像識(shí)別部分的記錄一下,交流學(xué)習(xí)。
一、2021電賽F題題目回顧與分析
1.題目介紹
因?yàn)橹唤榻B視覺部分,我們就節(jié)選相關(guān)的部分吧。
設(shè)計(jì)并制作智能送藥小車,模擬完成在醫(yī)院藥房與病房間藥品的送取作業(yè)。院區(qū)結(jié)構(gòu)示意如圖 1 所示。院區(qū)走廊兩側(cè)的墻體由黑實(shí)線表示。走廊地面上畫有居中的紅實(shí)線,并放置標(biāo)識(shí)病房號(hào)的黑色數(shù)字可移動(dòng)紙張。藥房和近端病房號(hào)(1、 2 號(hào))如圖 1 所示位置固定不變,中部病房和遠(yuǎn)端病房號(hào)(3-8 號(hào))測(cè)試時(shí)隨機(jī)設(shè)定。
工作過程:參賽者手動(dòng)將小車擺放在藥房處(車頭投影在門口區(qū)域內(nèi),面向病房),手持?jǐn)?shù)字標(biāo)號(hào)紙張由小車識(shí)別病房號(hào),將約 200g 藥品一次性裝載到送藥小車上;小車檢測(cè)到藥品裝載完成后自動(dòng)開始運(yùn)送;小車根據(jù)走廊上的標(biāo)識(shí)信息自動(dòng)識(shí)別、尋徑將藥品送到指定病房(車頭投影在門口區(qū)域內(nèi)),點(diǎn)亮紅色指示燈,等待卸載藥品;病房處人工卸載藥品后,小車自動(dòng)熄滅紅色指示燈,開始返回;小車自動(dòng)返回到藥房(車頭投影在門口區(qū)域內(nèi),面向藥房)后,點(diǎn)亮綠色指示燈。
2.圖像部分分析
由題意可知,圖像部分大致可以分為
- 識(shí)別道路
- 路中央紅線巡線
- 路口識(shí)別
- 終點(diǎn)線黑色虛線識(shí)別
- 識(shí)別數(shù)字
- 開始位置識(shí)別數(shù)字
- 路口識(shí)別兩個(gè)數(shù)字
- 路口識(shí)別四個(gè)數(shù)字
2.1識(shí)別道路
識(shí)別道路有很多方案,我們組前期錯(cuò)誤的選擇了紅外循線的方案。這種方案精度低,而且會(huì)受環(huán)境影響。
后期轉(zhuǎn)向OpenMV的方案。
2.2識(shí)別數(shù)字
識(shí)別數(shù)字有很多方案,比如OpenMV、K210、樹莓派、Jetson nano甚至x86架構(gòu)的單板計(jì)算機(jī)都可以用,但是因?yàn)榍捌跍?zhǔn)備的原因我們只實(shí)現(xiàn)了OpenMV的方案。
這里還是要說一下,OpenMV算力有限,實(shí)在是難堪重任,并不是本題的最優(yōu)解法。
二、識(shí)別道路部分
1.巡線-紅色實(shí)線
這里我們采用的是匿名飛控給無人機(jī)寫的一套OpenMV代碼,略作修改。
核心思想是在圖像的上、中、下、左、右各劃出一個(gè)細(xì)長(zhǎng)條的區(qū)域,在各自區(qū)域內(nèi)檢測(cè)是否有指定大小的紅色色塊,再根據(jù)五個(gè)部分紅色色塊的有無即可判定是直線還是路口、是何種路口以及直線的傾角和偏移量。
如下圖所示,左邊只有上、中、下有小方框,是直線;右邊上、中、下、左、右都有小方框,是路口。
2.終點(diǎn)線-黑色虛線
終點(diǎn)線是黑色虛線,可以視為兩厘米見方的黑色小矩形,可以使用OpenMV內(nèi)置的矩形檢測(cè)函數(shù)檢測(cè)指定大小范圍的矩形,當(dāng)矩形數(shù)量足夠多時(shí)即視為終點(diǎn)線。
如下圖所示,識(shí)別到六個(gè)以上矩形塊即可視為終點(diǎn)線。
3.代碼實(shí)現(xiàn)
import sensor, image, time, math, struct
from pyb import UART
import json
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
# 要檢測(cè)顏色,所以使用彩色模式
sensor.set_framesize(sensor.QQVGA)
# 使用QQVGA降低畫質(zhì)提升運(yùn)行速度
sensor.skip_frames(time=3000)
sensor.set_auto_whitebal(False)
# 顏色檢測(cè)一定要關(guān)閉自動(dòng)白平衡
clock = time.clock()
uart = UART(1, 115200)
uart.init(115200, bits=8, parity=None, stop=1)
# 上面是串口通信的部分
Red_threshold =[(13, 40, -2, 57, 11, 47),(29, 50, 13, 79, 15, 67),(33, 50, 16, 73, 2, 61)]
# 紅色的LAB閾值,在賽場(chǎng)上需要重新進(jìn)行標(biāo)定,可使用OpenMV IDE自帶的閾值編輯器進(jìn)行標(biāo)定
ROIS = {
'down': (0, 105, 160, 15),
'middle': (0, 52, 160, 15),
'up': (0, 0, 160, 15),
'left': (0, 0, 15, 120),
'right': (145,0, 15, 120),
'All': (0, 0, 160,120),
}
# 劃分了上中下左右五個(gè)部分
class LineFlag(object):
flag = 0
cross_y = 0
delta_x = 0
class EndFlag(object):
endline_type = 0
endline_y = 0
LineFlag=LineFlag()
EndFlag=EndFlag()
# 紅色實(shí)線部分函數(shù)
def find_blobs_in_rois(img):
global ROIS
roi_blobs_result = {}
for roi_direct in ROIS.keys():
roi_blobs_result[roi_direct] = {
'cx': -1,
'cy': -1,
'blob_flag': False
}
for roi_direct, roi in ROIS.items():
blobs=img.find_blobs(Red_threshold, roi=roi, merge=True, pixels_area=10)
if len(blobs) == 0:
continue
largest_blob = max(blobs, key=lambda b: b.pixels())
x,y,width,height = largest_blob[:4]
if not(width >=3 and width <= 45 and height >= 3 and height <= 45):
continue
roi_blobs_result[roi_direct]['cx'] = largest_blob.cx()
roi_blobs_result[roi_direct]['cy'] = largest_blob.cy()
roi_blobs_result[roi_direct]['blob_flag'] = True
if (roi_blobs_result['down']['blob_flag']):
if (roi_blobs_result['left']['blob_flag']and roi_blobs_result['right']['blob_flag']):
LineFlag.flag = 2 #十字路口或丁字路口
elif (roi_blobs_result['left']['blob_flag']):
LineFlag.flag = 3 # 左轉(zhuǎn)路口
elif (roi_blobs_result['right']['blob_flag']):
LineFlag.flag = 4 # 右轉(zhuǎn)路口
elif (roi_blobs_result['middle']['blob_flag']):
LineFlag.flag = 1 #直線
else:
LineFlag.flag = 0 # 未檢測(cè)到
else:
if(roi_blobs_result['middle']['blob_flag']and roi_blobs_result['up']['blob_flag']):
if (roi_blobs_result['left']['blob_flag']and roi_blobs_result['right']['blob_flag']):
LineFlag.flag = 5 # 即將跨過十字路口
elif (roi_blobs_result['left']['blob_flag']):
LineFlag.flag = 6 # 即將跨過左拐丁字路口
elif (roi_blobs_result['right']['blob_flag']):
LineFlag.flag = 7 # 即將跨過右拐丁字路口
else:
LineFlag.flag = 8 # 直線(無down塊)
else:
LineFlag.flag = 0
# 下面這部分是特例的判斷,防止出現(xiàn)
# “本來是直線道路,但是太靠近左側(cè)或者右側(cè),被識(shí)別成了左轉(zhuǎn)或者右轉(zhuǎn)”
# 這樣的特殊情況
if (LineFlag.flag == 3 and roi_blobs_result['left']['cy']<10):
LineFlag.flag = 1
if (LineFlag.flag == 4 and roi_blobs_result['right']['cy']<10):
LineFlag.flag = 1
if (LineFlag.flag == 3 and roi_blobs_result['down']['cx']<30):
LineFlag.flag = 1
if (LineFlag.flag == 4 and roi_blobs_result['down']['cy']>130):
LineFlag.flag = 1
# 計(jì)算兩個(gè)輸出值,路口交叉點(diǎn)的縱坐標(biāo)和直線時(shí)紅線的偏移量
LineFlag.cross_y = 0
LineFlag.delta_x = 0
if (LineFlag.flag == 1 or LineFlag.flag == 2 or LineFlag.flag == 3 or LineFlag.flag == 4) :
LineFlag.delta_x = roi_blobs_result['down']['cx']
elif (LineFlag.flag == 5 or LineFlag.flag == 6 or LineFlag.flag == 7 or LineFlag.flag == 8):
LineFlag.delta_x = roi_blobs_result['middle']['cx']
else:
LineFlag.delta_x = 0
if (LineFlag.flag == 2 or LineFlag.flag == 5):
LineFlag.cross_y = (roi_blobs_result['left']['cy']+roi_blobs_result['right']['cy'])//2
elif (LineFlag.flag == 3 or LineFlag.flag == 6):
LineFlag.cross_y = roi_blobs_result['left']['cy']
elif (LineFlag.flag == 4 or LineFlag.flag == 7):
LineFlag.cross_y = roi_blobs_result['right']['cy']
else:
LineFlag.cross_y = 0
# 終點(diǎn)線黑色虛線部分的函數(shù)
def find_endline(img):
endbox_num = 0
for r in img.find_rects(threshold = 10000):
endbox_size = r.magnitude()
endbox_w = r.w()
endbox_h = r.h()
k=1
# 篩選黑色矩形大小
if (endbox_size<24000*k*k and endbox_h<25*k and endbox_w<25*k) :
endbox_num = endbox_num + 1;
# 判斷是否是終點(diǎn)線
EndFlag.endline_type = 0
if (endbox_num>2 and endbox_num<6):
EndFlag.endline_type = 1 # 檢測(cè)到第一條終點(diǎn)線
elif(endbox_num >=6 ):
EndFlag.endline_type = 2 # 檢測(cè)到兩條終點(diǎn)線
else:
EndFlag.endline_type = 0 # 未檢測(cè)到終點(diǎn)線
while(True):
clock.tick()
global img
img = sensor.snapshot()
# 拍照
img = img.replace(vflip=1,hmirror=1,transpose=0)
# 因?yàn)槭堑寡b的做上下顛倒
find_blobs_in_rois(img)
# 巡線函數(shù)
find_endline(img)
# 找終點(diǎn)線函數(shù)
FH = bytearray([0xc3,0xc3])
uart.write(FH)
# 發(fā)送幀頭
data = bytearray([LineFlag.flag, LineFlag.delta_x, LineFlag.cross_y, EndFlag.endline_type])
uart.write(data)
# 發(fā)送內(nèi)容
ED = bytearray([0xc4,0xc4])
uart.write(ED)
# 發(fā)送幀尾
4.接口定義
Line.flag
數(shù)值 | 含義 |
---|---|
00 | 未檢測(cè)到直線 |
01 | 直線 |
02 | 十字路口或丁字路口 |
03 | 左轉(zhuǎn)路口(頂部10像素以下) |
04 | 右轉(zhuǎn)路口(頂部10像素以下) |
05 | 即將跨過十字路口(無down塊) |
06 | 即將跨過左拐丁字路口(無down塊) |
07 | 即將跨過右拐丁字路口(無down塊) |
08 | 直線(無down塊) |
LineFlag.delta_x
數(shù)值 | 含義 |
---|---|
0~160 | 賽道紅色中心線底部的X軸水平位置,左小右大;無down塊時(shí),返回中部的middle塊X軸水平位置 |
LineFlag.cross_y
數(shù)值 | 含義 |
---|---|
0~120 | 賽道紅色中心線十字路口或丁字路口交叉點(diǎn)的Y軸豎直位置,上小下大 |
EndFlag.endline_type
數(shù)值 | 含義 |
---|---|
00 | 未檢測(cè)到終點(diǎn)線 |
01 | 檢測(cè)到第一根終點(diǎn)線 |
02 | 檢測(cè)到第二根終點(diǎn)線 |
三、識(shí)別數(shù)字部分
1.總體思路
1.1 識(shí)別方法
由題目可知,我們要同時(shí)識(shí)別兩個(gè)或四個(gè)數(shù)字,這里有很多辦法,我們的辦法是讓相機(jī)盡可能加高、使用廣角鏡頭以便同時(shí)能看到四個(gè)數(shù)字。
如下圖所示,小車的高度剛好卡在了25cm的限高,以便同時(shí)看到四個(gè)數(shù)字。
我們?cè)賵D像內(nèi)劃分出了五個(gè)ROI區(qū)域,依次檢測(cè),即可檢測(cè)到處于五種不同位置的數(shù)字了。
1.2 模型訓(xùn)練
OpenMV可以跑TensorFlow Lite模型,具體如何訓(xùn)練可以參考下面這篇博客。
https://blog.csdn.net/qq_36300069/article/details/118071444
訓(xùn)練模型的網(wǎng)站如下
https://studio.edgeimpulse.com/
這個(gè)網(wǎng)站可以把模型打包好導(dǎo)入OpenMV中,數(shù)據(jù)集是自己拍的照片,一共八十張訓(xùn)練集、二十張測(cè)試集。參數(shù)上我選擇的是160*160像素、灰度圖、訓(xùn)練100步,模型選的0.35的V2模型。
數(shù)據(jù)集如下圖所示,左邊是訓(xùn)練集,右邊是測(cè)試集。
訓(xùn)練結(jié)束后取得了不錯(cuò)的效果,識(shí)別準(zhǔn)確率都在70%以上。
1.3 圖像處理
僅僅是神經(jīng)網(wǎng)絡(luò)模型識(shí)別準(zhǔn)確率還不高,我們使用了一些方法對(duì)圖像進(jìn)行一些處理來提高識(shí)別的成功率。
- 鏡頭畸變校正
- 縮小圖像(避免畫面損失)
- 翻轉(zhuǎn)圖像(因?yàn)榈寡b)
- 透視校正
- 反相、紅色填充黑色后再反相(去除紅色影響)
如下圖所示,左側(cè)是未處理的圖像,右圖是處理后的圖像。
總體流程如下
文章來源:http://www.zghlxwxcb.cn/news/detail-611525.html
2.代碼實(shí)現(xiàn)
import sensor, image, time, os, tf
from pyb import UART
import json
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.VGA)
sensor.skip_frames(time=2000)
net = "trainedv13.tflite"
# 透視校正用的四個(gè)點(diǎn)
TARGET_POINTS = [(143,210),
(495,214),
(640,480),
(0,480)]
# 識(shí)別數(shù)字的五個(gè)區(qū)域
ROI0 = (210,170,170,170)
ROI1 = (20,0,170,170)
ROI2 = (150,0,170,170)
ROI3 = (285,0,170,170)
ROI4 = (430,0,170,170)
# 反相后紅色閾值
xred_threshold = (51, 84, -31, -3, -26, -2)
# 各區(qū)域識(shí)別數(shù)字準(zhǔn)確度門檻
keyline_0 = 0.7
keyline_1 = 0.6
keyline_2 = 0.65
keyline_3 = 0.65
keyline_4 = 0.6
ans_num = 0
clock = time.clock()
uart = UART(3, 115200)
uart.init(115200, bits=8, parity=None, stop=1)
while(True):
clock.tick()
# 拍照并進(jìn)行一堆預(yù)處理
img = sensor.snapshot().lens_corr(strength = 1.7, zoom = 0.55)
img = img.replace(vflip=1,hmirror=1,transpose=0)
img = img.rotation_corr(corners = TARGET_POINTS)
img = img.negate()
img = img.binary([xred_threshold], invert=False, zero=True)
img = img.negate()
# 識(shí)別中心數(shù)字
for obj in tf.classify(net, img, roi=ROI0, min_scale=1.0, scale_mul=0.8, x_overlap=0.5, y_overlap=0.5):
out = obj.output()
max_idx = out.index(max(out))
if max(out)>keyline_0:
ans_0 = max_idx + 1
else:
ans_0 = 0
# 識(shí)別左起第一個(gè)數(shù)字
for obj in tf.classify(net, img, roi=ROI1, min_scale=1.0, scale_mul=0.8, x_overlap=0.5, y_overlap=0.5):
out = obj.output()
max_idx = out.index(max(out))
if max(out)>keyline_1:
ans_1 = max_idx + 1
else:
ans_1 = 0
# 識(shí)別左起第二個(gè)數(shù)字
for obj in tf.classify(net, img, roi=ROI2, min_scale=1.0, scale_mul=0.8, x_overlap=0.5, y_overlap=0.5):
out = obj.output()
max_idx = out.index(max(out))
if max(out)>keyline_2:
ans_2 = max_idx + 1
else:
ans_2 = 0
# 識(shí)別左起第三個(gè)數(shù)字
for obj in tf.classify(net, img, roi=ROI3, min_scale=1.0, scale_mul=0.8, x_overlap=0.5, y_overlap=0.5):
out = obj.output()
max_idx = out.index(max(out))
if max(out)>keyline_3:
ans_3 = max_idx + 1
else:
ans_3 = 0
# 識(shí)別左起第四個(gè)數(shù)字
for obj in tf.classify(net, img, roi=ROI4, min_scale=1.0, scale_mul=0.8, x_overlap=0.5, y_overlap=0.5):
out = obj.output()
max_idx = out.index(max(out))
if max(out)>keyline_4:
ans_4 = max_idx + 1
else:
ans_4 = 0
# 串口通信模塊
FH = bytearray([0xc3,0xc3])
uart.write(FH)
data = bytearray([ans_1, ans_2, ans_3, ans_4, ans_0])
uart.write(data)
ED = bytearray([0xc4,0xc4])
uart.write(ED)
3.識(shí)別效果
實(shí)際識(shí)別效果尚可,但是幀率極低,聯(lián)機(jī)狀態(tài)只有大約0.4fps。文章來源地址http://www.zghlxwxcb.cn/news/detail-611525.html
四、總結(jié)反思
- 只識(shí)別兩個(gè)區(qū)域即可。四個(gè)數(shù)字的路口如果檢測(cè)不到就隨便去一個(gè),走錯(cuò)了再掉頭就好。可以提升幀率。
- 結(jié)合上一條,如果有兩塊OpenMV各自識(shí)別一個(gè)數(shù)字,幀率提升更明顯。
- 結(jié)合上一條,將OpenMV換成K210,識(shí)別效果和幀率會(huì)更好。實(shí)際完賽的組大多是使用這個(gè)方法的。
- 如果使用樹莓派或者jetson nano自然更好,也有部分組別用了這個(gè)方法。
- 電賽控制題日趨智能化,樹莓派和神經(jīng)網(wǎng)絡(luò)模型將會(huì)逐漸成為常態(tài)。
到了這里,關(guān)于2021電賽F題送藥小車視覺部分的一種思路(雙OpenMV法)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!