已经是七月中旬,黄宁然,你依然在这里。
[1]龚思宇宙,基于平面模板的摄像机标定及相关技术研究
[2]汪洋,扫地机器人定位算法设计与嵌入式系统实现
[3]Denny#,【opencv】goodFeaturesToTrack源码分析-1
[4]Denny#,【opencv】goodFeaturesToTrack源码分析-2-Shi-Tomasi角点检测
注:csdn发文助手提示“外链过多”,故文献链接网址请见评论区。
笔者的执念。
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。
参阅文献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)
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不是,这导致保留的角点位置、对后续周边角点的抑制会存在些许差异)。
对这几次在角点检测时用到非极大值抑制代码进行了总结,写了几种极大值抑制代码。其中方法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],
# 从抑制结果可看出,2和5之间距离是小于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
Python程序源码下载地址
https://download.csdn.net/download/xiaohuolong1827/85847444