前
本文作者保留所有权利
背景
第十届上海市大学生工程训练综合能力竞赛 · 配送无人机选题
正
这个比赛,我其实有很大划水的成分,我一个电子电气工程学院的跑航空学院做一个飞行类的比赛,似乎也有些难施拳脚。比赛筹备过程中,我主要负责降落阶段的落点目标识别(主要分三类,第一类为印刷体字母标记,第二类为同心圆、第三类为人、车或房的卡通图片)。当时那个阶段我正在研究K210这款搭载KPU的平台,自然而然的想到了用Yolo算法进行训练,没准能达到所需要的的效果。可匆忙的临门一脚的结果便是,效果不尽人意。我对人工智能这块仅停留于不久之前的一门导论课,而人工智能的根本:概率论和线性代数,我也学的一刀烂。
事实证明,我对Yolo算法的实践还停留于跑出一个识别率并不高的模型,在实际运用中,模型对于相同场景下但未在训练集中的视频帧的识别结果置信度并不高(只有可怜的0.4左右)。从两方面考量,一是训练集仅有大约两百多张图片,要实现精准的目标识别还存在问题,尤其是Yolo算法中较为关键的Anchor参数(见文Yolo Anchor 数值 聚类分析);第二点是K210所适配的摄像头约为240P,实际输入进模型的帧大小为224*320,数据量小,摄像头最低照度参数差。最终还是被我否决了。
经过团队里的头脑风暴,我们大概总结了以下几种方案。
基于Yolo算法自行训练- 基于Minist数据集进行识别
- 基于SIFT算法的图片匹配
基于Minist数据集进行识别,我觉得方法很巧,这里也简单写写,基本思路是将所识别的物体参照Minist集进行分割。比如字母标记,毋庸置疑,可以直接用Minist集进行训练。对于那些人、车和房,可以抓住其中长得像字母的部分,做色调分离后的二值化。唯一不方便的是,可能需要根据赛事议题进行调整模型的训练集。识别时置信度比较高,几乎达到了1。
基于SIFT算法的图片匹配主要由我来实施,测试单例如下:
import numpy as np
import cv2 #在OpenCV 3.4之后因专利版权问题移除了SIFT/SURF的相关库,本文稍后会附上相关说明。
def load_image(path, gray=False):
if gray:
img = cv2.imread(path)
return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
else:
return cv2.imread(path)
def transform(origin):
h, w, _ = origin.shape
generate_img = np.zeros(origin.shape)
for i in range(h):
for j in range(w):
generate_img[i, w - 1 - j] = origin[i, j]
return generate_img.astype(np.uint8)
def main():
# 数据要求。两个图片要大小一致,色彩空间一致。如800px^2,RGB 32位色。比赛的时候就将img1设定为拍摄到的,并且编辑成和树莓派CAM帧大小一致的图片,可以留白,目前测试不影响。img2就取视频帧即可。
img1 = load_image('Source.png')
img2 = load_image('Target.jpg')
# 实例化
sift = cv2.xfeatures2d.SIFT_create()
# 计算关键点和描述子
# 其中kp为关键点、des为描述子
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# 绘出关键点
# 其中参数分别是源图像、关键点、输出图像、显示颜色
img3 = cv2.drawKeypoints(img1, kp1, img1, color=(0, 255, 255)) # 在源图片上标注关键点
img4 = cv2.drawKeypoints(img2, kp2, img2, color=(0, 255, 255)) # 在目标图片上标注关键点
# 参数设计和实例化
index_params = dict(algorithm=1, trees=6)
search_params = dict(checks=40)
flann = cv2.FlannBasedMatcher(index_params, search_params)
# 利用knn计算两个描述子的匹配
matche = flann.knnMatch(des1, des2, k=2)
matchesMask = [[0, 0] for i in range(len(matche))]
# 绘出匹配效果
result = []
resultItem = []
for m, n in matche:
if m.distance < 0.3 * n.distance: #判断欧式距离,按照SIFT理论,关键点向量越接近,欧式距离越短,即两点越像。 常数0.3可以继续调整,数字越大,匹配的点位越多,也越不精准。
result.append([m])
resultItem.append(m)
img5 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, result, None, flags=2)
# combine = np.hstack((img3, img4))
# cv2.imshow("KeyPoints", combine)
cv2.imshow("K", img5)
for m in resultItem:
print(kp2[m.queryIdx].pt) # 该参数为目标图片中关键点的坐标。
cv2.waitKey(15000)
if __name__ == '__main__':
main()
测试过程中,正好在吃麦当劳,就用麦当劳里的LOGO做了一次测试。(这里请麦当劳给我打钱,谢谢。)
嗯 效果还不错。
接下来就是结合视频帧进行处理了,逻辑也很简单,一个死循环,不断读视频帧,然后处理,输出结果。代码如下:
import numpy as np
import cv2
def load_image(path, gray=False):
if gray:
img = cv2.imread(path)
return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
else:
return cv2.imread(path)
def transform(origin):
h, w, _ = origin.shape
generate_img = np.zeros(origin.shape)
for i in range(h):
for j in range(w):
generate_img[i, w - 1 - j] = origin[i, j]
return generate_img.astype(np.uint8)
def main():
cap = cv2.VideoCapture(0)
imgSource = load_image('source.png')
# 实例化
imgSource = cv2.resize(imgSource, None, fx=0.5, fy=0.5, interpolation = cv2.INTER_CUBIC)
sift = cv2.xfeatures2d.SIFT_create()
kpSource, desSource = sift.detectAndCompute(imgSource, None)
imgSourceWithKp = cv2.drawKeypoints(imgSource, kpSource, desSource, color=(0, 255, 255))
index_params = dict(algorithm=1, trees=6)
search_params = dict(checks=100)
flann = cv2.FlannBasedMatcher(index_params, search_params)
while(True):
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation = cv2.INTER_CUBIC)
kpFrame, desFrame = sift.detectAndCompute(frame, None)
imgFrameWithKp = cv2.drawKeypoints(frame, kpFrame, frame, color=(0, 255, 255))
matche = flann.knnMatch(desSource, desFrame, k=2)
#matchesMask = [[0, 0] for i in range(len(matche))]
# 绘出匹配效果
result = []
resultItem = []
for m, n in matche:
if m.distance < 0.4 * n.distance: #判断欧式距离,按照SIFT理论,关键点向量越接近,欧式距离越短,即两点越像。 常数0.3可以继续调整,数字越大,匹配的点位越多,也越不精准。
result.append([m])
resultItem.append(m)
img5 = cv2.drawMatchesKnn(imgSource, kpSource, frame, kpFrame, result, None, flags=2)
#combine = np.hstack((imgSourceWithKp, imgFrameWithKp))
#cv2.imshow("KeyPoints", combine)
cv2.imshow("K", img5)
for m in resultItem:
print(kpFrame[m.queryIdx].pt) # 该参数为目标图片中关键点的坐标。可以对半分。这个相信你们应该懂的。
if cv2.waitKey(1) & 0xFF == ord('q'):
break
print("Next Frame")
cap.release()
cv2.distoryAllWindows()
if __name__ == '__main__':
main()
嗯,感觉识别率还可以。
但这个颜色,感觉有这么点阴间。
用一个简单的通道均值算法来解决这个问题。
def whiteBalance(frame):
r, g, b = cv2.split(frame)
r_avg = cv2.mean(r)[0]
g_avg = cv2.mean(g)[0]
b_avg = cv2.mean(b)[0]
k = (r_avg + g_avg + b_avg) / 3
kr = k / r_avg
kg = k / g_avg
kb = k / b_avg
r = cv2.addWeighted(src1=r, alpha=kr, src2=0, beta=0, gamma=0)
g = cv2.addWeighted(src1=g, alpha=kg, src2=0, beta=0, gamma=0)
b = cv2.addWeighted(src1=b, alpha=kb, src2=0, beta=0, gamma=0)
return cv2.merge([b, g, r])
插曲
这次市赛突然修改了规则,识别物从原先的$500mm^2$变成了$250mm^2$。因此大部分参赛队伍都是手动飞的,只有一组顶流大学,使用的是SLAM方案,自动完成了比赛,实在是佩服。
从算法角度来说,使用SURF代替SIFT可以获得更高性能,并可以考虑使用完整帧。
关于SIFT算法专利版权问题
OpenCV 3.4之后因专利版权问题移除了SIFT/SURF的相关库,因此在使用较新版本的CV库时会报错。
而网上如今绝大多数解决方法都是回退OpenCV的版本并且安装Contrib支持库,这样还意味着你需要回退或者创建一个支持3.4之前的Python版本的环境(3.5、3.6),总而言之非常麻烦!
而在2020年3月17日之后,SIFT专利到期了,因此只需更新OpenCV版本即可免费使用。
推荐将原先的:cv2.xfeatures2d.SIFT_create()
改成cv2.SIFT_create()
。