一.源码
1.MaixCAM源码
话不多说,先给出源码看能懂多少。
################################################################################
#三点法巡线
################################################################################
from maix import camera, display, image,time,app
from maix.v1.machine import UART
import math
import struct
########################串口UART初始化################################
uart = UART("/dev/ttyS0", 115200)
time.sleep_ms(100) # wait uart ready
#uart.write(b'hello zpc')
################################摄像头初始化################################
cam = camera.Camera(320, 240) #灰度化
disp = display.Display()
Threshold=[[0, 31, -2, 21, -30, 2]] #黑色路线阈值,可使用MaixCAM拍照,将照片放在OpenMv-IDE里去调阈值
#//这个ROI区域根据自己的实际情况去改变
ROIS=[(0,20,360,60,0.2), #上
(0,90,360,60,0.4), #中
(0,170,360,50,0.9)] #下
weight_sum = 0
for r in ROIS: weight_sum += r[4] #权重
line_rho=0 #偏移量
################################巡线################################
def car_run():
global line_rho
line_rho=0
line_blobs=[] #色块存放的列表
centroid_sum = 0 #权重
for r in ROIS:
blobs= img.find_blobs(Threshold, roi=r[0:4], merge=True,pixels_threshold=150)
if blobs:
for b in blobs:
line_blobs.append(b) #加入 line_blobs 列表
centroid_sum += b.cx() * r[4] #计算centroid_sum,centroid_sum等于
#每个区域的色块的中心点的x坐标值乘
#本区域的权值
line_rho = int(centroid_sum / weight_sum) #中间值公式
#用于调试
img.draw_string(0,30,' rho= '+str(line_rho),image.COLOR_WHITE)
return line_blobs
#标记函数
def Mark(Line):
for b in Line:
img.draw_rect(b.x(),b.y(),b.w(),b.h(),color=image.COLOR_WHITE)
img.draw_cross(b.cx(),b.cy(),image.Color.from_rgb(255, 255, 255), size=5, thickness=1)
################################发送数据################################
def Uart_Send(rho):
'''
rho2=0
if rho>252:
rho2=rho-252
rho=252
data=bytes([0xFD,rho,rho2,0xFE])
'''
#半字,rho>256也可以使用,可以去网上了解一下,如果使用半字,上面的if条件请删除
data=struct.pack(">BHB",0xFD,rho,0xFE)
uart.write(data)
line_element=[]
while not app.need_exit():
t = time.time_ms()
img=cam.read()
line_element=car_run()
Mark(line_element)
#数据发送
Uart_Send(line_rho)
#屏幕显示
disp.show(img)
#打印帧率
print("FPS= ",int(1000 / (time.time_ms() - t)))
代码效果如图:
2.OpenMV源码
##############################################################
#三点法巡线
##############################################################
import sensor, image,time,math
from pyb import UART
from pyb import LED
usart1=UART(1,115200) #P0-TX发送 P1-RX接收
usart1.init(115200, bits=8, parity=None, stop=1) #初始化串口
#关闭补光灯防止地面反光
LED(1).off()
LED(2).off()
LED(3).off()
sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE) #灰度图
sensor.set_framesize(sensor.QQVGA) #160*120
sensor.set_auto_gain(False) #颜色跟踪必须关闭自动增益
sensor.set_auto_whitebal(False) #颜色跟踪必须关闭白平衡
sensor.skip_frames(time = 3000) #跳过3秒等待感光元件设置生效
clock = time.clock()
#循迹黑色色块阈值:
black = (63, 0) #循迹黑色赛道阈值
black_roi= [(0, 20, 160, 20, 0.1), #利用3个roi获取赛道rho、thera
(0, 50, 160, 30, 0.2), #3个roi用于巡线,获取模拟量
(0, 85, 160, 30, 0.9)] #元组第四位代码权重,离小车越近的roi权重越高
weight_sum = 0 #权值和初始化
for r in black_roi: weight_sum += r[4]
global line_rho
line_rho=0 #rho变量
#得到巡线模拟量函数:
def car_run(img):
global line_rho
line_blobs=[]
centroid_sum = 0 #权重
for r in black_roi: #利用颜色识别分别寻找三个矩形区域内的线段
black_blobs = img.find_blobs([black], roi=r[0:4],y_stride=10,area_threshold=100, merge=True) #merge=True-表示合并所有重叠的色块
if black_blobs:
for b in black_blobs:
line_blobs.append(b) #加入 line_blobs 列表
centroid_sum += b.cx() * r[4] #计算centroid_sum,centroid_sum等于
#每个区域的最大颜色块的中心点的x坐标值乘
#本区域的权值
line_rho = int(centroid_sum / weight_sum) #中间值公式
return line_blobs
#标记函数:
def Mark(Line):
#【1】标记巡线:
for blob in Line:
img.draw_rectangle(blob.rect()) #圈出巡线区块
img.draw_cross(blob.cx(), blob.cy()) #圈出中心十字
#终端打印函数:
def UART_Send(rho):
#数据包,可自行修改,可保持原样
data=bytes([0xFD,rho,0xFE])
usart1.write(data)
########################################### 主函数 ################################################
while(True):
clock.tick()
img = sensor.snapshot().lens_corr(1.8) #拍照,畸变矫正1.8
Line = car_run(img) #从图像中得到赛道rho
Mark(Line) #进行标记
UART_Send(line_rho)
print(clock.fps())
效果如图:
二.介绍
1.总体
三点法巡线的一种常用巡线方法,还有一种方法为线性回归。三点法通过划分三个ROI,然后在这三个区域里识别色块,通过权重*中心坐标,然后除以总权重,及得到了偏移量,通过pid调节偏移量,即可达到巡线的目的。同样的方法我们可以算出角度,我们先在这卖个关子,在后面扩展中,我会介绍。
2.ROI感兴趣区域
学习ROI可以参考OpenMV学习文档使用统计信息 · OpenMV中文入门教程
roi的格式是(x, y, w, h)的tupple.
- x:ROI区域中左上角的x坐标
- y:ROI区域中左上角的y坐标
- w:ROI的宽度
- h:ROI的高度
ROI感兴趣区域顾名思义,及划分一个区域给机器识别,而其他区域不识别。
3.如何编辑阈值
阈值是识别色块的必要条件,我单独列出来是告诉你如何编辑阈值。
方法一:MaixCAM APP编辑
找到MaixCAM自带的Find blobs APP
点击你要识别的色块,点左下角的阈值查看,按顺序填入即可。
但是这种方法有一个缺陷,受环境影响比较大,自动采集可能不如手动编辑。
方法二:使用OpenMV IDE 的阈值编辑器
如下图,找到阈值编辑器
如果使用的是OpenMV,就选帧缓冲区去调。
用其他的视觉开发板则先保存文件,然后选择图像文件打开然后编译即可。
调节方式:
1.重置色块
2.一个一个划,如果出现不了指定物体的轮廓(要白色像素,不要黑色轮廓),就先不管这个参数,调其他的参数,当出现差不多的轮廓,再去调其他的。
如图:(调第二个出现了轮廓)
3.其他的参数调节,有两种方式。
方式一:随便调,调到有大概的理想效果就行。
方法二:一个参数调到只要随便动一下就会变黑的效果。(识别效果稍好,但抗干扰性较弱)
如图:(调节的参数是 A的最大值)
灰度阈值调节同理。
4.偏移量
三点法主要巡线逻辑就是偏移量。
如图,根据公式计算出权重偏移量。
for r in ROIS:
blobs= img.find_blobs(Threshold, roi=r[0:4], merge=True,pixels_threshold=150)
if blobs:
for b in blobs:
line_blobs.append(b) #加入 line_blobs 列表
centroid_sum += b.cx() * r[4] #计算centroid_sum,centroid_sum等于 #本区域的权值
line_rho = int(centroid_sum / weight_sum) #中间值公式
5.总结
三点法巡线,并没有那么难理解,主要逻辑就是分区域找色块,然后通过权重计算偏移量,这就是三点法巡线的核心。
三.拓展
如果你觉得三点法很简单,那又错了。视觉巡线追求的是”快准稳“。如果你巡线的偏移量摆动幅度过大,或者是色块识别不准,误识别等,而你只会三点法,那怎么办?三点法相当于一个地基,地基是建筑的开始,而上层建筑是衡量的主要标准。
会一个三点法巡线还远远不够。单纯讲三点法又过于简单,所以,我将通过实例来介绍一下好用的提升巡线效率算法。
1.22年电赛C题
不谈其他的,我们只讲摄像头巡线。
问题:1.如何准确识别到黑线,以及防止光照影响。(如果抗光照能力弱,调的时候很好,但换个场地就不行了,这就是一个致命失误。视觉将背大锅的。)
2.如何区分岔路口,等停指示和终点?
3.如何提高视觉巡线效率的同时帧率稳定?
2.解决方案
问题一:如何准确识别到黑线,以及防止光照影响。
在最上面的源码中,我只进行了对色块识别,没有加入其他的算法。黑线最有特征的就是黑,如果因为反光而导致”黑“不够”黑“,那我是否可以让它变”黑“?当然,通过二值化的方法我可以让黑线变得更”黑“,这样一来就解决了反光导致识别率降低的问题。
给出OpenMV文档的二值化例程作为参考学习,本文只讲方法不讲原理与函数使用
color_binary_filter 二值化滤波 · OpenMV中文入门教程
img=cam.read().binary(Threshold, invert=False, zero=True) #MaixCAM
在调节阈值的时候,将L最小值置0,识别效果更好。
问题二:如何区分岔路口,等停标志和终点?
在摄像头的视角里只有一条线时,保证好只有三个色块,当识别到岔路口时,则色块数量会大于3个,此时就可以视为岔路口标志位。
等停标志和终点可以通过色块的像素面积以及w宽度来限定,在摄像头固定后,一般来说等停标志和终点的色块像素面积和w宽度能保持在一个区间内。用if语句即可判断等停标志和终点。
识别岔路口后如何区分内外圈?一般如果对视觉不熟悉就直接识别岔路口,让软件去通过开环直接写死开过去,例如读这段距离的编码器累计值等。
当然看到这篇文章我会分享一个对软件友好的对视觉不太友好的算法。
在一开始识别色块的时候,我就识别两个偏移量,当视角内只有一条线那这两个偏移量相同,当出现岔路口时,一个偏移量是内线,另一个偏移量则是外线。
如何区分这六个色块?
请看代码:
def find_max(blobs):
max_size=[0,0]
max_ID=[-1,-1]
for i in range(len(blobs)):
if blobs[i].pixels()>max_size[0]:
max_ID[1]=max_ID[0]
max_size[1]=max_size[0]
max_ID[0]=i
max_size[0]=blobs[i].pixels()
elif blobs[i].pixels()>max_size[1]:
max_ID[1]=i
max_size[1]=blobs[i].pixels()
return max_ID
def car_run():
centroid_sum = [0,0]
left_center=[-1,-1,-1] #存放左边色块的中心cx值用于计算左边的偏移角
right_center=[-1,-1,-1] #存放右边色块的中心cx值用于计算右边的偏移角
flag_cross=0 #是否有分岔口
flag_Stop=0 #停止标志
flag_Wait=[0,0] #等待停止标志
for r in range(3): #三个区域分别寻找色块
blobs = img.find_blobs(Threshold, roi=ROIS[r][0:4], merge=True,pixels_threshold=1100)
if blobs:
max_ID=[-1,-1]
max_ID=find_max(blobs) #找最大色块
if blobs[max_ID[0]].w()<100:
img.draw_rect(blobs[max_ID[0]].x(), blobs[max_ID[0]].y(), blobs[max_ID[0]].w(), blobs[max_ID[0]].h(),color=image.COLOR_WHITE)
img.draw_cross(blobs[max_ID[0]].cx(), blobs[max_ID[0]].cy(), image.Color.from_rgb(255, 0, 0), size=5, thickness=1) #圈出中心十字-红
if max_ID[1]!=-1: #如果有两个色块,即有分岔口,分成左边右边存入数组
if blobs[max_ID[1]].w()<100 :
img.draw_rect(blobs[max_ID[1]].x(), blobs[max_ID[1]].y(), blobs[max_ID[1]].w(), blobs[max_ID[1]].h(),color=image.COLOR_GREEN)
img.draw_cross(blobs[max_ID[1]].cx(), blobs[max_ID[1]].cy(), image.Color.from_rgb(255, 255, 255), size=5, thickness=1) #圈出中心十字-红
flag_cross=1
if blobs[max_ID[0]].cx()<blobs[max_ID[1]].cx():
left_center[r]=blobs[max_ID[0]].cx()
right_center[r]=blobs[max_ID[1]].cx()
else:
left_center[r]=blobs[max_ID[1]].cx()
right_center[r]=blobs[max_ID[0]].cx()
else: #只有一个色块
#print(blobs[max_ID[0]].pixels(),blobs[max_ID[0]].w())
if flag_cross==0: #没有分岔口,进行判断停止线
if blobs[max_ID[0]].pixels()>range_stop[r]:
flag_Stop=r+1
if blobs[max_ID[0]].w()>range_wait[r]:
flag_Wait[0]=flag_Wait[0]+1
left_center[r]=right_center[r]=blobs[max_ID[0]].cx()
centroid_sum[0] += left_center[r] * ROIS[r][4] #乘权值
centroid_sum[1] += right_center[r] * ROIS[r][4]
center_pos =[0,0]
center_pos[0] = centroid_sum[0] / weight_sum
center_pos[1] = centroid_sum[1] / weight_sum
if flag_Wait[0]==2:
flag_Wait[1]=1
deflection_angle = [0,0]
deflection_angle[0] = -math.atan((center_pos[0]-180)/120)#计算角度
deflection_angle[1] = -math.atan((center_pos[1]-180)/120)
deflection_angle[0] = math.degrees(deflection_angle[0])#弧度制换成角度制
deflection_angle[1] = math.degrees(deflection_angle[1])
if center_pos[0]==center_pos[1]==0:
deflection_angle[1]=deflection_angle[0]=0
A=[int(deflection_angle[0])+90,int(deflection_angle[1])+90,flag_Stop,flag_Wait[1],int(center_pos[0]),int(center_pos[1]),flag_cross]
return A
如何理解这个算法,可以看这篇文章:2022年电赛C题小车之OpenMV篇_vs codeopenmv-CSDN博客
问题三:如何提高视觉巡线效率的同时保持帧率稳定?
在问题一中,我给出了二值化的方法来提高准确率与抗干扰性。但这对摄像头帧率影响比较大,使用MaixCAM帧率在20~30帧左右(320,240).当然巡线是完完全全够用了。但达到够用的程度,不是我写这篇文章的初衷,我想告诉大家,方法有很多种。虽然没有完美的方案,但一定有比你更优秀的方案。如果你在摄像头这方面效果很好,那么对软件的要求将会降低很多,也就能在更短的时间内调完。显然,如果摄像头抗干扰能力弱,影响了测评,写摄像头的人就是背所有锅。
提高帧率最好的方法无疑是降低画质,但降低画质带来的后果就是精确度降低,根据题目来判断你要对什么做出取舍,对于小车题,我推荐你降低画质来保证帧率稳定。在我调试过程中,(320,240)精度比较高,更容易转弯,(160,120)帧率更高,走直线更稳定。两个画质看实际情况选择。
前面说到二值化会降低帧率,而且二值化会识别”阴影“。一开始我一直用的二值化,因为我觉得一般测评的时候不会有人站在地图上的,所以识别”阴影“影响不会很大。但后面我进行灰度化处理就解决了这个问题,灰度与二值化类似,但灰度是改变画面,而二值化是对图像做处理。显然我直接改变画质是快于二值化的,改变画质只要开头初始化,而二值化就要在主函数里一直调用。所以灰度化会比二值化帧率高。
cam = camera.Camera(160, 120,image.Format.FMT_GRAYSCALE)
但在使用灰度化后又出现了一个问题,就是反光,当光照强时,反光亮度高,光照弱,反光就弱,很大影响了识别色块效果。所以,为了解决这个问题,我们引出另一个滤波--均值滤波。
同样我不介绍均值滤波,我推荐你看这篇文档,学习均值滤波的用法:
mean_filter 均值滤波 · OpenMV中文入门教程
均值滤波顾名思义,通过均匀化阈值,达到减弱反光效果。
img.mean(1) #均值滤波
均值滤波可以提升识别准确率。