写这篇文章主要是记录我的备战日常、遇到的问题以及解决办法方便自己以后翻阅。同时也希望能给学弟学妹们留下点什么,不会像我一样遇到问题的时候不知道应该怎么办。
可能会有很多问题,欢迎大家指出来,相互学习。
持续更新
四月·算法实现
本人在本次大赛中主要承担视觉的部分,所以往后的文章也会围绕视觉方面展开。由于队内传承,算法方面多借鉴上交AuTop战队的开源,文章的最开始也会致力于实现AuTop算法的实现。
第16届智能车智能视觉组-上海交通大学AuTop战队开源汇总 - 知乎 (zhihu.com)
为方便,我将利用python语言实现各算法。
一、视觉具体任务
图像二值化 -> 边线提取 -> 标定和透视变换 -> 边线处理 -> 元素识别 -> 路径规划
二、图像二值化
1. 图像的编码
想要将图像二值化就要先了解计算机是如何处理图片的。
图片的编码方式多样,这里只介绍RGB与灰度图像。
关于图片编码可以查看这篇文章,个人认为说的很详细了。
图像格式RGB/HSV/YUV - 知乎 (zhihu.com)
RGB -> 图片可以看作是三个二维数组的叠加,每一个数组代表三原色中的一种(R、G、B),而二维数组中的每一位代表一个像素点,从而每个像素点的颜色就可以由三个数组叠加形成。
灰度图像 -> 与RGB图像不同,灰度图像的每个像素点的颜色只由白黑两种颜色占比决定,所以我们可以只使用一个二维数组就表示了一整个图像。
我们以后要处理的图像即是灰度图像。
2. 对于二值化的一些理解
1.什么是图像二值化?
-> 图像二值化的本质是图像分割,就是将图像背景和前景分割开的过程。这里也可以简单理解为把灰度图像变为黑白图像的过程。
2.为什么要二值化?
-> 图像二值化的优势在于方便后续对图像的处理。当然,图像的二值化并非必然,我曾经就看到一篇基于灰度图像处理赛道边界的文章,当中使用的方法为识别灰度值的变化,而二值化后仅需要判断像素点是黑/白即可。由于没有对灰度图像处理的经验,我们仍采用大众方式将其二值化。
3.二值化的方法
-> 二值化的方法有很多,这里我们采用最常用的阈值法对图像进行二值化处理。即通过算法求出一个阈值,对图像每个像素点做如下处理:灰度值>阈值,赋255(黑色) ;灰度值<阈值,赋0(白色) 。处理后的图像将会是由0和255组成的二维数组,即图像由纯黑白两种颜色组成。
3. 算法实现
1.图像初始化
基于openCV导入图片的操作,并将图像转化为灰度图像。
#图像的初始化
import cv2
img = cv2.imread("xx.jpg") # 生成图片
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) #灰度图像
img_I = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化后的图像储存位
HIGH = len(img_gray) - 1 # 处理的图像的像素-1
LONG = len(img_gray[0]) - 1
cv2.imshow("Image", img_I) # 显示图像
cv2.waitKey() # 等待图片关闭
2.二值化的实现
通过比较阈值与灰度值,实现图像分割。
即:
f
(
p
i
l
e
x
)
=
{
255
,
f
(
p
i
l
e
x
)
>
t
h
r
e
s
h
o
l
d
0
,
f
(
p
i
l
e
x
)
<
t
h
r
e
s
h
o
l
d
f(pilex)=\begin{cases} 255,& f(pilex)>threshold\\ 0,&f(pilex)<threshold\\ \end{cases}
f(pilex)={255,0,f(pilex)>thresholdf(pilex)<threshold
其中pilex即当前点的灰度值,threshold即为阈值的大小。
"""图像二值化"""
img_high = 0 # 正在计算的像素
img_long = 0
while img_high != HIGH or img_long != LONG: # 判断是否为右下角
if img_I[img_high][img_long] > threshold: # 判断像素与阈值的大小 --> 二值化
img_I[img_high][img_long] = 255
else:
img_I[img_high][img_long] = 0
if img_long == LONG:
img_high += 1
img_long = 0
# print(img_high) # 显示进度条
else:
img_long += 1
3.固定阈值法
即直接给出一个阈值,对所有图像采用同一阈值进行二值化处理。但是由于室内不用位置亮度不同、不同时间点的亮度不同,采用这种方法几乎无法应对时空变化的影响。
4.全局均值阈值法
由于固定阈值不能够适应时空的变化对亮度(即阈值)的影响,所以我们希望对每一帧图像进行单独计算阈值。
根据名字理解,就是求全图灰度值的平均值对图像进行分割,这种方式的优势在于简单,且运算量极低。但这种算法的适应性较差,由于赛道共两种颜色,如果两种颜色的占比相差很大,阈值会被占比大的一方拉低/拉高,这样就会导致一定程度上的误判。
(PS:这里使用中值理论上也可以实现对图像的分割)
"""全局均值阈值法"""
pixel_sum = 0 # 总灰度值
pilex_number = 0 # 计算的总格数
img_high = 0 # 正在处理的像素点
img_long = 0
pixel = 0 # 正在处理的像素点的灰度值
threshold = 0 # 阈值
for a in img_gray: # 遍历图像
for pilex in a:
pixel_sum += pilex # 计算总灰度值
pilex_number += 1 # 更新已经计算的格数
threshold = pixel_sum / pilex_number # 计算阈值
# print(threshold)
5.大津法算阈值
针对上一个全局均值阈值法,我们希望使用一种更科学的算法来找出两种颜色的分割点作为阈值。所以我们引入了大津法来求取阈值。如果我们绘制一个灰度直方图来统计各灰度值有多少个像素点,不难发现图像会大体上呈现双峰形状。这主要是因为图像主要有两种颜色构成,而每种颜色对应一个灰度值。那么如果我们能够找到两峰中的某一点作为阈值,就能够将背景与赛道分割开。
关于大津法网上有太多的教程了,无法讲的很详细,在这里我放出几个当时我看的链接。
Otsu算法——最大类间方差法(大津算法)
大津法(OTSU 最大类间方差法)详细数学推导(公式繁杂,欢迎讨论)_
otsu算法详细推导、实现及Multi Level OTSU算法实现_
最后是代码实现
"""大津法"""
def Otsu ():
gray = [0] * 256 # 储存各个灰度值的个数
# gray_sum_foreground = 0 # 储存 灰度*个数 前景
# gray_sum_background = 0 # 储存 灰度*个数 背景
# foreground_sum = 0 # 前景像素个数
# background_sum = 0 # 后景像素个数
# m = 0 # 前景与背景的分割点
# maxmium = 0 # 类间方差法结果计算位
au = 0 # 类间方差法结果比较位
threshold = 0 # 阈值储存位
for a in img_gray: # 遍历图像 --> 创建灰度值对应个数的列表
for pilex in a:
gray[pilex] += 1 # 更新该灰度值的个数
for m in range(0, 255): # 遍历全部灰度值 --> 寻找最大类间方差法点
foreground_sum = 0 # 前景像素个数
background_sum = 0 # 后景像素个数
gray_sum_foreground = 0 # 储存 灰度*个数 前景
gray_sum_background = 0 # 储存 灰度*个数 背景
for n in range(0, m):
gray_sum_foreground += gray[n] * n
foreground_sum += gray[n]
for n in range(m, 255):
gray_sum_background += gray[n] * n
background_sum += gray[n]
w0 = foreground_sum / (HIGH + 1 * LONG + 1) # 计算前景平均灰度
w1 = background_sum / (HIGH + 1 * LONG + 1) # 计算背景平均灰度
if foreground_sum == 0: # 排除当前灰度值的个数为0的计算报错
m0 = 0
else:
m0 = gray_sum_foreground / foreground_sum # 计算前景像素点占比
if background_sum == 0:
m1 = 0
else:
m1 = gray_sum_background / background_sum # 计算背景像素点占比
maxmium = w0 * w1 * (m0 - m1) ** 2 # 计算类间方差
if maxmium >= au: # 与目前为止最大的类间方差比较
threshold = m # 更新阈值
au = maxmium # 更新最大类间方差值
# print(threshold)
return threshold
PS: 如果采用先寻找两个高峰,再寻找两个高峰之间的最低点的方式,在数据允许的情况下可以减少计算量,同时也能够获得相对有效的二值化结果。
6.自适应阈值
上面的算法已经能够很好的对一张图像进行二值化处理,但是实际过程中会由于反光等因素,导致图像的某片区域亮度明显高于其他区域。自适应阈值算法的提出是为了解决图像各位置亮度不同,即排除光线对二值化的影响。如图为经过大津法处理后的图像:
如果直接对整图进行同一二值化处理会出现白块/黑块,导致赛道信息丢失,之所以会出现这种现象是由于对整张图像算出的阈值可能无法完美适配于图像的全部区域。即如果图像上有一块亮区,那么这块区域的赛道相对这块区域而言为黑,但对全图而言很有可能会被识别为白,全图二值化后会将该区域直接拉白,但是如果我们只对这一片区域处理,就可以在其中提取到相对黑的赛道。效果图如下:
算法的思路:取一个像素点,计算以这一点为中心n×n方阵的灰度值的均值作为这一点的阈值,然后将这个操作遍历全图。对于边线,我的处理方式是对边线进行识别,识别到超出范围后,将取消对超出范围部分的计算。
算法的缺点:由于要对每一个像素点进行单独求阈值,所以导致计算的复杂度急剧上升,对性能要求极高,无法满足对整图进行处理。
代码如下:
"""自适应二值化"""
def erzhihua (high, long):
pixel_sum = 0 # 总像素
pilex_number = 0 # 计算的总格数
pilex_max = 3 # 前后左右步进的格数
long_min = long - pilex_max # 左上角坐标
high_min = high - pilex_max
long_max = long + pilex_max # 右下角坐标
high_max = high + pilex_max
if long_min < 0: # 判断左上角边线
long_min = 0
if high_min < 0:
high_min = 0
if long_max > LONG: # 判断右下角边线
long_max = LONG
if high_max > HIGH:
high_max = HIGH
high = high_min # 将正在计算的坐标初始化为左上角
long = long_min
while long != long_max or high != high_max: # 判断是否为右下角
pixel_sum += img_gray[high][long] # 将该点像素加到总像素
pilex_number += 1 # 已经计算的格数
if long == long_max: # 如果行满
high += 1
long = long_min
else:
long += 1
threshold = (pixel_sum / pilex_number)-3 # 计算阈值
return threshold
到这里我们图像二值化的部分就基本完成了,一个好的二值化图像将是能否成功识别边线的重要因素,如果未来尝试新的二值化方式也会更新在这里。