Shi-tomasi角点检测python实现及基于opencv实现 (角点检测、非极大值抑制)

写在前面:

已经是七月中旬,黄宁然,你依然在这里。

参考文献镇楼:

[1]龚思宇宙,基于平面模板的摄像机标定及相关技术研究
[2] 扫地机器人
[3]Denny#,【opencv】goodFeaturesToTrack源码分析-1
[4]Denny#,【opencv】goodFeaturesToTrack源码分析-2-Shi-Tomasi角点检测
注:csdn发文助手提示“外链过多”,故文献链接网址请见评论区。

问题来源:

笔者的执念。

1、原理简介

Shi-tomasi算子是对Harris算子的改进。根据Harris算法流程(详见上一篇博客),角点响应值R是根据自相关矩阵M的行列式与迹来求取的,算法的稳定性和k值有关。
Shi和Tomasi发现,角点的稳定性和自相关矩阵M的较小特征值有关,因此,Shi-tomasi剔除的改进方法是:如果自相关矩阵M的两个特征值中较小的那个,超过了设定阈值,则认为是强角点。所以,shi-tomasi算法的角点响应函数为[1]:
R=min(λ1 , λ2)
Shi-tomasi算法在计算步骤上,也与Harris基本相同,主要步骤如下(详见上一篇博客):
(1)计算图像在水平方向和垂直方向的导数 Ix 和 Iy 以及 I x 2 I_x^2 Ix2 I y 2 I_y^2 Iy2 I x I_x Ix y _y y
(2)对上述元素进行平滑滤波,得到自相关矩阵M。
(3)求M的的特征值,并将较小的那个作为该角点的响应值 R。
(4)对于各响应值R,若大于设定好的阈值,则该R对应的坐标即为初步的角点。
该阈值的选取:找到所有R中的最大值λmax(即所有较小特征值中最大的那个),然后将k*λmax最为阈值。
(5)对初步的角点,进行后续的非极大值抑制处理。这里nms分两部分。第一部分,使用3*3的矩形窗,仅保留窗口内的局部最大值,完成一次nms;第二部分,根据设置的角点间距minDistannce,同时结合最大角点数maxCorners,进行第二次nms。

2、python源码实现

参阅文献3和文献4,基于python自行编写代码实现shi-tomasi角点检测。其中,计算角点响应之前的代码,与harris基本一致,可参阅上一篇博客。

#coding=utf8
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import math
import cv2
import os,sys
import scipy.ndimage
import time
import scipy

编写shi-tomasi角点检测函数,函数的各参数参考opencv源码设置,其中,maxCorners为最多检测角点数,qualityLevel,为质量系数,该数值乘以λmax(即所有较小特征值中最大的那个)即阈值;minDistance,为角点间的允许最小距离,以便进行nms;blockSize为处理窗口大小(固定窗口在不同方向移动、监测窗内灰度变化情况的那个窗口的大小,也是对偏导数求和时的窗大小);k为harris算子系数。该函数部分参数的默认值与opencv源码保持一致。

def my_good_featuressToTrack(img_src,maxCorners,qualityLevel,minDistance,mask=None,blockSize=3,useHarrisDetector=False,k=0.04):
	method = 'harris'if useHarrisDetector else 'shi tomasi'
	corner_arr = np.zeros(img_src.shape) #角点结果,为0时,为角点;非0时,为角点
	# 1. 首先求取角点响应矩阵
	eigen_val = calc_corner_eigen_value(img_src, block_size=blockSize, aperture_size=3, k=k, borderType=cv2.BORDER_DEFAULT,option=method)
	# 2. 设置阈值门限,对低于门限的归0
	max_v = np.max(eigen_val)
	thr_v = max(max_v * qualityLevel,0)
	eigen_val[eigen_val<thr_v]=0
	# 3. 如果设置了mask,做相应的mask处理
	if mask!=None:
		x,y = np.where(mask==0)
		eigen_val[x,y] = 0
	# 4. 第一次nms:借助膨胀运算来寻找局部最大值,局部矩形窗口为3*3
	kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
	dilate_val = cv2.dilate(eigen_val,kernel=kernel)
	x,y = np.where(eigen_val==dilate_val)#如果原始值与膨胀之后的值相等,说明原始值即是局部最大值
	index = np.where(eigen_val[x,y]!=0)#剔除等于0的位置
	x,y = x[index],y[index]
	#找到局部最大值,并保存(所以非局部最大值,便自动剔除了,相当于nms)
	corner_arr[x,y] = eigen_val[x,y]
	# 5. 根据minDistance取值进行二次nms
	corner_arr = array_nms_circle(corner_arr, minDistance,maxCount = maxCorners)#minDistance=0时,相当于不nms
	x, y = np.where(corner_arr != 0)
	out_arr = np.array([x,y,corner_arr[x,y]]).transpose() # 转换为 r,c,val 的格式

	return list(out_arr)

计算角点响应矩阵

def calc_corner_eigen_value(img_src,block_size=2,aperture_size=3,k=0.04,borderType=cv2.BORDER_DEFAULT,option='harris'):
	if img_src.dtype!=np.uint8:
		raise ("input image shoud be uint8 type")
	R_arr = np.zeros(img_src.shape,dtype=np.float32)#用来存储角点响应值分情况,得分为0时,表示不是角点
	img = img_src.astype(np.float32)
	scale = 1.0/( (aperture_size-1)*2*block_size*255 )
	#借助sobel算子,计算x、y方向上的偏导数
	Ix = cv2.Sobel(img,-1,dx=1,dy=0,ksize=aperture_size,scale=scale,borderType=borderType)
	Iy = cv2.Sobel(img,-1,dx=0,dy=1,ksize=aperture_size,scale=scale,borderType=borderType)
	#求平方即相乘
	Ixx = Ix**2
	Iyy = Iy**2
	Ixy = Ix*Iy
	#借助boxFilter函数,以block_size为窗口大小,对窗口内的数值求和,且不归一化
	f_xx = cv2.boxFilter(Ixx,ddepth=-1,ksize=(block_size,block_size) ,anchor =(-1,-1),normalize=False, borderType=borderType)
	f_yy = cv2.boxFilter(Iyy,ddepth=-1,ksize=(block_size,block_size),anchor =(-1,-1),normalize=False,borderType=borderType)
	f_xy = cv2.boxFilter(Ixy, ddepth=-1,ksize=(block_size,block_size),anchor =(-1,-1),normalize=False,borderType=borderType)
	# 也可以尝试手动求和
	radius = int((block_size - 1) / 2)  # 考虑blocksize为偶数的情况,奇数时,前、后的数量一样;为偶数时,后比前多一个
	N_pre = radius
	N_post = block_size - N_pre - 1
	row_s, col_s = N_pre, N_pre
	row_e, col_e = img.shape[0] - N_post, img.shape[1] - N_post

	for r in range(row_s,row_e):
		for c in range(col_s,col_e):
			#手动对窗口内的数值求和
			#sum_xx = Ixx[r-N_pre:r+N_post+1,c-N_pre:c+N_post+1].sum()
			#sum_yy = Iyy[r-N_pre:r+N_post+1,c-N_pre:c+N_post+1].sum()
			#sum_xy = Ixy[r-N_pre:r+N_post+1,c-N_pre:c+N_post+1].sum()
			#或者直接使用boxFilter的结果
			sum_xx = f_xx[r,c]
			sum_yy = f_yy[r, c]
			sum_xy = f_xy[r, c]

			if option=='harris':#根据行列式和迹求响应值
				det = (sum_xx * sum_yy) - (sum_xy ** 2)
				trace = sum_xx + sum_yy
				res = det - k * (trace ** 2)
				# 或者用如下方式求行列式和迹
				#M = np.array([[sum_xx,sum_xy],[sum_xy,sum_yy]])
				#res = np.linalg.det(M) - (k *  (np.trace(M))**2 )
				R_arr[r,c] = res
			else: #求特征值
				root_min = 0.5*(sum_xx+sum_yy) - 0.5*np.sqrt( (sum_xx-sum_yy)**2 + 4*(sum_xy**2) )#仅仅求方程较小的那个根
				R_arr[r, c] = root_min
	return R_arr

nms代码

def array_nms_circle(dataIn,radius=1,maxCount=None):#在以radius为半径的圆形区域内,进行nms,并取前maxCount个
	# 先计算圆形区域下标mask_x,mask_y
	distance = np.zeros((radius*2+1,radius*2+1))
	x_label = np.arange(-radius,radius+1)
	y_label = np.arange(-radius,radius+1)
	for i in range(0,2*radius+1):
		distance[i,:] = x_label[i]**2 + y_label**2
	x,y = np.where(distance<=radius**2) #找出距离中心点距离小于等于radius的位置,即圆形区域内的所有下标
	mask_x,mask_y = x-radius,y-radius #对找到的位置下标进行中心化
	# 开始进行nms
	# 思路,1.找到最大值位置,2.保存该位置(因为全局最大,肯定是极大值)
	# 3.以该位置为中心,radius为半径,将该圆内的数据全部归零抑制
	# 4.重复1~3,直至全局的最大值小于等于0(或达到maxCount数量)。则所有保存的位置即是经过nms后的值
	data = dataIn.copy()
	rows,cols = data.shape
	out_arr = np.zeros(data.shape)
	count = 0
	while(np.max(data)>0):
		r, c = np.unravel_index(data.argmax(), data.shape)#获取最大值位置
		zone_r,zone_c = r+mask_x,c+mask_y #获取圆形区域位置
		index1 = np.logical_and(zone_r>=0,zone_r<rows)#剔除越界的位置
		index2 = np.logical_and(zone_c>=0,zone_c<cols)#剔除越界的位置
		index =  np.logical_and(index1,index2)#剔除越界的位置
		zone_r,zone_c = zone_r[index],zone_c[index]#剔除越界的位置
		out_arr[r,c] = data[r,c] #先保存位置
		data[zone_r,zone_c] = 0 #对该区域抑制归零,为寻找下一个最大值做准备
		count = count+1
		if ( maxCount!=None and count>=maxCount):#达到数量后,则停止
			break
	return out_arr

主程序调用:

if __name__ == '__main__':
	img_src = cv2.imread('rubix1.jpg',cv2.IMREAD_GRAYSCALE)
	#使用自行编写的代码计算角点
	corners = my_good_featuressToTrack(img_src, maxCorners=1000, qualityLevel=0.01, minDistance=7)
	print(len(corners))
	img_show = cv2.cvtColor(img_src, cv2.COLOR_GRAY2BGR)
	for i in corners:
		x, y,v = i
		img_show[int(x),int(y)] =  (255, 0, 0)
	plt.figure()
	plt.title("corners")
	plt.imshow(img_show, cmap=cm.gray)

检测结果如下:
在这里插入图片描述

3、基于opencv实现

Opencv自带shi-tomas算子,可以直接检测角点。这里使用的opencv版本为3.4.2.16
调用方式为:

corners= cv2.goodFeaturesToTrack(img_src, maxCorners=1000,qualityLevel=0.01,  minDistance=7)

主程序调用:

if __name__ == '__main__':
	img_src = cv2.imread('rubix1.jpg',cv2.IMREAD_GRAYSCALE)
	#img_src = img_src[190:210,80:100]
	#img_src = img_src[35:90,100:150]
	#使用自行编写的代码计算角点
	corners = my_good_featuressToTrack(img_src, maxCorners=1000, qualityLevel=0.01, minDistance=7)
	print(len(corners))
	img_show = cv2.cvtColor(img_src, cv2.COLOR_GRAY2BGR)
	for i in corners:
		x, y,v = i
		img_show[int(x),int(y)] =  (255, 0, 0)
	plt.figure()
	plt.title("corners")
	plt.imshow(img_show, cmap=cm.gray)

	# 使用opencv源码计算角点
	corners_opencv = cv2.goodFeaturesToTrack(img_src, maxCorners=1000, qualityLevel=0.01, minDistance=7)
	print(len(corners_opencv))
	img_show2 = cv2.cvtColor(img_src, cv2.COLOR_GRAY2BGR)
	for i in corners_opencv:
		x, y = i.ravel()
		img_show2[int(y),int(x)] =  (255, 0, 0)
	plt.figure()
	plt.title("opencv")
	plt.imshow(img_show2, cmap=cm.gray)

	plt.show()

检测结果为:
在这里插入图片描述
将自行编写的shi-tomasi角点检测代码运行结果与opencv运行结果相对比,二者在角点数量上基本一致(存在些许不一致时,一个可能的原因是:在进行nms时,需要求取矩阵的最大值,若矩阵中有多个相同的最大值,则python返回的首次出现位置,而opencv不是,这导致保留的角点位置、对后续周边角点的抑制会存在些许差异)。

4、关于非极大值抑制

对这几次在角点检测时用到非极大值抑制代码进行了总结,写了几种极大值抑制代码。其中方法1和方法2存在些许问题,具体见代码的注释。

def corner_nms1(corner,kernal=3):
	# 思路:对于每个位置,以该位置为中心点,选取kernal*kernal的矩形窗,在该窗内,如果该中心点不是最大值,则将其抑制归零
	# 然后,计算一下个位置(所以矩形窗是在滑动的,步长为1)
	# 存在问题,举例,对于一维数组[0,1,2,3,4,5,6],kenral选择为3,按照上述思路,经过nms后的结果为[0,0,0,0,0,0,6]
	# 这种情况下,抑制的最厉害,得到的结果也较少,但可能不是我们所期望的结果
	out = corner.copy()
	row_s = int(kernal/2)
	row_e = out.shape[0] - int(kernal/2)
	col_s,col_e = int(kernal/2),out.shape[1] - int(kernal/2)
	for r in range(row_s,row_e):
		for c in range(col_s,col_e):
			if corner[r,c]==0: #不是可能的角点
				continue
			zone = corner[r-int(kernal/2):r+int(kernal/2)+1,c-int(kernal/2):c+int(kernal/2)+1]
			index = corner[r,c]<zone
			(x,y) = np.where(index==True)
			if len(x)>0 : #说明corner[r,c]不是最大,直接归零将其抑制
				out[r,c] = 0
	return out
def array_nms2(data,kernal=3):
	# 思路:将输入矩阵,分成若干个kernal*kernal的方块,
	# 每个方块内,找到最大值位置,保存(其它位置,不保存,相当于抑制)
	# 也就是方块与方块之间,是独立的
	# 这也存在问题,举例,对于一维数组[0,1,2,3,5,4],按照该思路,nms后的结果为[0,0,2,0,5,0],
	# 从抑制结果可看出,25之间距离是小于kernal的,应该抑制掉一个
	rows,cols = data.shape
	if kernal>rows or kernal >cols:
		raise ("ERROR:kernal size can not be greater than data shape")
	out_arr = np.zeros(data.shape)
	r_st,c_st=0,0
	while(r_st<rows):
		r_ed =  (r_st+kernal) if (r_st+kernal)<rows else rows
		c_st = 0
		while(c_st<cols):
			c_ed = (c_st+kernal) if (c_st+kernal)<cols else cols
			zone = data[r_st:r_ed,c_st:c_ed]
			r,c=np.unravel_index(zone.argmax(),zone.shape)
			out_arr[r+r_st,c+c_st] = zone[r,c]
			c_st = c_st+kernal
		r_st = r_st + kernal
	return out_arr
def array_nms_rect(dataIn,radius=1):#在以(2*radius+1)x(2*radius+1)的矩形区域内进行nms
	# 思路,1.找到全局最大值位置,2.保存该位置(因为全局最大,肯定是极大值)
	# 3.以该位置为中心,取(2*radius+1)x(2*radius+1)的矩形区域,将该区域内的数据全部归零抑制
	# 4.重复1~3,直至全局的最大值小于等于0。则所有保存的位置即是经过nms后的值
	data = dataIn.copy()
	rows,cols = data.shape
	out_arr = np.zeros(data.shape)
	while(np.max(data)>0):
		r, c = np.unravel_index(data.argmax(), data.shape)
		r_st,c_st = (r-radius), (c-radius)#计算矩形窗起始坐标
		r_ed,c_ed = r+radius,c+radius#计算矩形窗结束坐标
		r_st = 0 if r_st<0 else r_st#剔除越界位置
		c_st = 0 if c_st<0 else c_st#剔除越界位置
		r_ed = (rows) if r_ed>(rows) else r_ed#剔除越界位置
		c_ed = (cols) if c_ed>(cols) else c_ed#剔除越界位置
		out_arr[r,c] = data[r,c]#保存最大值位置
		data[r_st:r_ed,c_st:c_ed] = 0#对该区域抑制归零,为寻找下一个最大值做准备
	return out_arr
def array_nms_circle(dataIn,radius=1,maxCount=None):#在以radius为半径的圆形区域内,进行nms,并取前maxCount个
	# 先计算圆形区域下标mask_x,mask_y
	distance = np.zeros((radius*2+1,radius*2+1))
	x_label = np.arange(-radius,radius+1)
	y_label = np.arange(-radius,radius+1)
	for i in range(0,2*radius+1):
		distance[i,:] = x_label[i]**2 + y_label**2
	x,y = np.where(distance<=radius**2) #找出距离中心点距离小于等于radius的位置,即圆形区域内的所有下标
	mask_x,mask_y = x-radius,y-radius #对找到的位置下标进行中心化
	# 开始进行nms
	# 思路,1.找到最大值位置,2.保存该位置(因为全局最大,肯定是极大值)
	# 3.以该位置为中心,radius为半径,将该圆内的数据全部归零抑制
	# 4.重复1~3,直至全局的最大值小于等于0(或达到maxCount数量)。则所有保存的位置即是经过nms后的值
	data = dataIn.copy()
	rows,cols = data.shape
	out_arr = np.zeros(data.shape)
	count = 0
	while(np.max(data)>0):
		r, c = np.unravel_index(data.argmax(), data.shape)#获取最大值位置
		zone_r,zone_c = r+mask_x,c+mask_y #获取圆形区域位置
		index1 = np.logical_and(zone_r>=0,zone_r<rows)#剔除越界的位置
		index2 = np.logical_and(zone_c>=0,zone_c<cols)#剔除越界的位置
		index =  np.logical_and(index1,index2)#剔除越界的位置
		zone_r,zone_c = zone_r[index],zone_c[index]#剔除越界的位置
		out_arr[r,c] = data[r,c] #先保存位置
		data[zone_r,zone_c] = 0 #对该区域抑制归零,为寻找下一个最大值做准备
		count = count+1
		if ( maxCount!=None and count>=maxCount):#达到数量后,则停止
			break
	return out_arr

5、python源码下载

Python程序源码下载地址
https://download.csdn.net/download/xiaohuolong1827/85847444

猜你喜欢

转载自blog.csdn.net/xiaohuolong1827/article/details/125859795
今日推荐