Python计算机视觉编程第二章 局部图像描述子


这一章节旨在寻找图像间的对应点和对应区域。本章将介绍用于图像匹配的两种局部描述子算法。

1 Harris角点检测

Harris角点检测算法(也称Harris&Stephens角点检测器)是一个极为简单的交点检测算法。该算法的主要思想是,如果像素周围显示存在多于一个方向的边,我们认为该点为兴趣点。该点就称为角点。

我们把图像域中点x上的对称半正定矩阵MI=MI(x)定义为: M I = I I T = [ I x I y ] [ I x I y ] = [ I x 2 I x I y I x I y I y 2 ] M_I=\nabla I\nabla I^T=\begin{bmatrix}I_x \\ I_y\end{bmatrix}\begin{bmatrix}I_x & I_y\end{bmatrix}=\begin{bmatrix}I_x^2 & I_xI_y \\ I_xI_y & I_y^2\end{bmatrix} 其中∇I为包含导数Ix和Iy的图像梯度。由于该定义,MI的秩为1,特征值为λ1=|∇I|2和λ2=0。现在对于图像的每个像素,我们可以计算出该矩阵。

选择权重矩阵W(通常为高斯滤波器Gσ),我们可以得到卷积: M I = W M I \overline{M}_I=W*M_I 该卷积的目的是得到MI在周围像素上的局部平均。计算出的矩阵MI-有称为Harris矩阵。W的宽度决定了在像素x周围的感兴趣区域。像这样在区域附近对矩阵MI-取平均的原因是,特征值会依赖于局部图像特性而变化。如果图像梯度在该区域变化,那么MI-的第二个特征值将不再为0.如果图像的梯度没有变换,MI-的特征值也不会变化。

取决于该区域∇I的值,Harris矩阵MI-的特征值有三种情况:
1)如果λ1和λ2都是很大的正数,则该x点为角点;
2)如果λ1很大,λ2≈0,则该区域内存在一个边,该区域内的平均MI-的特征值不会变化太大;
3)如果λ1≈λ2≈0,则该区域为空。

在不需要实际计算特征值的情况下,为了把重要的情况和其他情况分开,Harris和Stephens在文献中引入了指示函数: d e t ( M I ) κ t r a c e ( M I ) 2 det(\overline{M}_I)-\kappa trace(\overline{M}_I)^2 为了去除加权常数κ,我们通常使用商数: d e t ( M I ) t r a c e ( M I ) 2 \frac{det(\overline{M}_I)}{trace(\overline{M}_I)^2} 作为指示器。

首先,我们先完成计算Harris角点检测器的响应函数:

def compute_harris_response(im, sigma=3):
    """在一幅灰度图像中,对每个像素计算Harris角点检测器响应函数"""
    
    # 计算导数
    imx = zeros(im.shape)
    filters.gaussian_filter(im, (sigma, sigma), (0,1), imx)    
    imy = zeros(im.shape)
    filters.gaussian_filter(im, (sigma, sigma), (1,0), imy)
    
    # 计算Harris矩阵的分量
    Wxx = filters.gaussian_filter(imx*imx, sigma)
    Wxy = filters.gaussian_filter(imx*imy, sigma)
    Wyy = filters.gaussian_filter(imy*imy, sigma)
    
    # 计算特征值和迹
    Wdet = Wxx * Wyy - Wxy**2
    Wtr = Wxx + Wyy
    
    return Wdet / Wtr

上面的函数会返回像素值为Harris响应函数值的一幅图像。现在,我们需要从这幅图像中挑选需要的信息。然后,选取像素值高于阈值的所有图像点;在加上额外的限制,即角点之间的间隔必须大于设定的最小距离。这种方法会产生很好的角点检测结果。为了实现该算法,我们获取所有候选像素点,以角点响应值递减的顺序排序,然后将距离已标记为角点位置过近的区域从候选像素点中删除:

def get_harris_points(harrisim, min_dist=10, threshold=0.1):
    """从一幅Harris响应图像中返回角点。min_dist为分割角点和图像边界的最小像素数目"""

    # 寻找高于阈值的候选角点
    corner_threshold = harrisim.max() * threshold
    harrisim_t = (harrisim > corner_threshold) * 1

    # 得到候选点的坐标
    coords = array(harrisim_t.nonzero()).T

    # 以及它们的Harris响应值
    candidate_values = [harrisim[c[0], c[1]] for c in coords]

    # 对候选点按照Harris响应值进行排序
    index = argsort(candidate_values)
    
    # 将可行点的位置保存到数组中
    allowed_locations = zeros(harrisim.shape)
    allowed_locations[min_dist:-min_dist,min_dist:-min_dist] = 1
    
    # 按照min_distance原则,选择最佳Harris点
    filtered_coords = []
    for i in index:
        if allowed_locations[coords[i,0], coords[i,1]] == 1:
            filtered_coords.append(coords[i])
            allowed_locations[(coords[i,0]-min_dist):(coords[i,0]+min_dist),
            (coords[i,1]-min_dist):(coords[i,1]+min_dist)] = 0
            
    return filtered_coords

使用harris角点检测器:

if __name__=='__main__':
    im = array(Image.open('D:\\123\图像处理\Image Processing\Image Processing\jimei1.jpg').convert('L'))
    harrisim = compute_harris_response(im)
    filtered_coords = get_harris_points(harrisim,6)
    plot_harris_points(im, filtered_coords)

在这里插入图片描述
在图像中寻找对应点

Harris角点检测器仅仅能够检测出图像中的兴趣点,但没有给出通过比较图像间的兴趣点来寻找匹配角点的方法。我们需要在每个点上加入描述子信息,并给出一个比较这些描述子的方法。

兴趣点描述子是分配给兴趣点的一个向量,描述该点附近的图像的表现信息。描述子越好,寻找到的对应点越好。我们用对应点或者点的对应来描述相同物体和场景点在不同图像上形成的像素点。

Harris角点的描述子通常是由周围图像像素块的灰度值,以及用于比较的归一化互相关矩阵构成的。图像的像素块由以该像素点为中心的周围矩形部分图像构成。

通常,两个(相同大小)像素块I1(x)和I2(x)的相关矩阵定义为: c ( I 1 , I 2 ) = X f ( I 1 ( x ) , I 2 ( x ) ) c(I_1,I_2)=\sum_Xf(I_1(x),I_2(x)) 其中,函数f随着相关方法的变化而变化。上式取像素块中所有像素位置x的和。对于互相关矩阵,函数f(I1,I2)=I1I2,因此,c(I1,I2)=I1·I2,其中·表示向量乘法。c(I1,I2)的值越大,像素块I1和I2的相似度越高。

归一化的互相关矩阵是互相关矩阵的一种变形,可以定义为: n c c ( I 1 , I 2 ) = 1 n 1 x ( I 1 ( x ) μ 1 ) σ 1 ( I 2 ( x ) μ 2 ) σ 2 ncc(I_1,I_2)=\frac{1}{n-1}\sum_x\frac{(I_1(x)-\mu_1)}{\sigma_1}\cdot \frac{(I_2(x)-\mu_2)}{\sigma_2} 其中,n为像素块中像素的数目,μ1和μ2表示每个像素块中平均像素值强度,σ1和σ2分别表示每个像素块中的标准差。通过减去均值和除以标准差,该方法对图像亮度变化具有稳健性。

def get_descriptors(image, filtered_coords, wid=5):
    """对于每个返回的点,返回点周围2*wid+1个像素的值(假设选取点的min_distance > wid)"""

    desc = []
    for coords in filtered_coords:
        patch = image[coords[0]-wid:coords[0]+wid+1,
                coords[1]-wid:coords[1]+wid+1].flatten()
        desc.append(patch)

    return desc

def match(desc1, desc2, threshold=0.5):
    """对于第一幅图像中的每个角点描述子,使用归一化互相关,选取它在第二幅图像中的匹配角点"""
    
    n = len(desc1[0])
    
    # 点对的距离
    d = -ones((len(desc1), len(desc2)))
    for i in range(len(desc1)):
        for j in range(len(desc2)):
            d1 = (desc1[i] - mean(desc1[i])) / std(desc1[i])
            d2 = (desc2[j] - mean(desc2[j])) / std(desc2[j])
            ncc_value = sum(d1 *d2) / (n-1)
            if ncc_value > threshold:
                d[i,j] = ncc_value
    
    ndx = argsort(-d)
    matchscores = ndx[:,0]
    
    return matchscores

第一个函数的参数为奇数大小长度的方形灰度图像块,该图像块的中心为处理的像素点。该函数将图像块像素值压平成一个向量,然后添加到描述子列表中。第二个函数使用归一化的互相关矩阵,将每个描述子匹配到另一个图像中的最优候选点。由于数值较高的距离代表两个点能够更好的匹配,所以在排序之前,我们对距离取相反数。为了获得更稳定的匹配,我们从第二幅图像向第一幅图像匹配,然后过滤掉在两种方法中都不是最好的匹配。下面的函数可以实现该操作:

def match_twosided(desc1, desc2, threshold=0.5):
    """两边对称版本的match()"""
    
    matches_12 = match(desc1, desc2, threshold)
    matches_21 = match(desc2, desc1, threshold)
    
    ndx_12 = where(matches_12 >= 0)[0]
    
    # 去除非对称的匹配
    for n in ndx_12:
        if matches_21[matches_12[n]] != n:
            matches_12[n] = -1
    
    return matches_12

这些匹配可以通过在两边分别绘制出图像,使用线段连接匹配的像素点来直观的可视化。下面代码可以实现匹配点的可视化。

def appendimages(im1, im2):
    """返回将两幅图像并排拼接成的一幅新图像"""
    
    # 选取具有最少行数的图像,然后填充足够的空行
    row1 = im1.shape[0]
    row2 = im2.shape[0]
    
    if row1 < row2:
        im1 = concatenate((im1,zeros((row2-row1,im1.shape[1]))), axis=0)
    elif row1 > row2:
        im2 = concatenate((im2,zeros((row1-row2,im2.shape[1]))), axis=0)
        
    # 如果这些情况都没有,那么他们的行数相同,不需要进行填充
    
    return concatenate((im1,im2), axis=1)

def plot_matches(im1, im2, locs1, locs2, matchscores, show_below=True):
    """显示一幅带有连接匹配之间连线的图片
        输入:im1,im2(数组图像),locs1,locs2(特征位置),matchscores(match的输出),
        show_below(如果图像应该显示再匹配下方)"""
    
    im3 = appendimages(im1,im2)
    if show_below:
        im3 = vstack((im3,im3))
        
    imshow(im3)
    
    cols1 = im1.shape[1]
    for i,m in enumerate(matchscores):
        if m > 0:
            plot([locs1[i][1],locs2[m][1]+cols1], [locs1[i][0], locs2[m][0]], 'c')
    axis('off')

实现

im1 = array(Image.open('D:\\123\图像处理\Image Processing\Image Processing\mona_target.jpg').convert('L'))
    im2 = array(Image.open('D:\\123\图像处理\Image Processing\Image Processing\mona_target.jpg').convert('L'))

    wid = 5
    harrisim = compute_harris_response(im1, 5)
    filtered_coords1 = get_harris_points(harrisim, wid+1)
    d1 = get_descriptors(im1, filtered_coords1, wid)

    harrisim = compute_harris_response(im2, 5)
    filtered_coords2 = get_harris_points(harrisim, wid + 1)
    d2 = get_descriptors(im1, filtered_coords2, wid)

    print('starting matching')
    matches = match_twosided(d1,d2)

    figure()
    gray()
    plot_matches(im1, im2, filtered_coords1, filtered_coords2, matches)
    show()

在这里插入图片描述
该算法的结果存在一些不正确匹配。这是因为,与现代的一些方法相比,图像像素块的互相关矩阵具有较弱的描述性。实际运用中,我们通常使用更稳健的方法来处理这些对应的匹配。这些描述符还有一个问题,他们不具有尺度不变性和旋转不变形,而算法中像素块的大小会影响对应匹配的结果。

近年来诞生了很多用来提高特征点检测和描述性能的方法。在下一小节,我们来学习其中最好的一种算法。

2 SIFT(尺度不变特征变换)

David Lowe提出的SIFT(Scale-Invariant Feature Transform,尺度不变特征变化)是过去十年中最成功的图像局部描述子之一。SIFT特征包括兴趣点检测器和描述子。SIFT描述子具有非常强的稳定性,这也是SIFT特征能够成功和流行的主要原因。SIFT特征对于尺度、旋转和亮度都具有不变性,因此,它可以用于三维视角和噪声的可靠匹配。

2.1 兴趣点

SIFT特征使用高斯差分函数来定位兴趣点: D ( x , σ ) = [ G κ σ ( x ) G σ ( x ) ] I ( x ) = I κ σ I σ D(x,\sigma)=[G_{\kappa \sigma}(x)-G_\sigma(x)]*I(x)=I_{\kappa \sigma}-I_\sigma 其中,Gσ是二维高斯核,Iσ是使用Gσ模糊的灰度图像,κ是决定相差尺度的常熟。兴趣点是在图像位置和尺度变换下D(x,σ)的最大值和最小值点。这些候选位置通过滤波去除不稳定点。基于一些准则,比如认为低对比度和位于边上的点不是兴趣点,我们可以去除一些候选兴趣点。

2.2 描述子

上面讨论的兴趣点(关键点)位置描述子给出了兴趣点的位置和尺度信息。为了实现旋转不变性,基于每个点周围图像梯度的方向和大小,SIFT描述子又引入了参考方向。SIFT描述子使用主方向描述参考方向。主方向使用方向直方图(以大小为权重)来度量。

2.3 检测兴趣点

我们使用开源工具包VLFeat提供的二进制文件来计算图像的SIFT特征。

def process_image(imagename, resultname, params="--edge-thresh 10 --peak-thresh 5"):
    """处理一幅图像,然后将结果保存在文件中"""
    
    if imagename[-3:] != 'pgm':
        #创建一个pgm文件
        im = Image.open(imagename).convert('L')
        im.save('tmp.pgm')
        imagename = 'tmp.pgm'
        
    cmmd= str("sift"+imagename+"--output="+resultname+" "+params)
    os.system(cmmd)
    print('processed',imagename,'to',resultname)

由于该二进制文件需要的图像格式为灰度.pgm,所以如果图像为其他各是,我们需要首先将其转换成.pgm格式文件。其中数据的每一行前4个数值依次表示兴趣点的坐标、尺度和方向角度,后面紧跟着的是对应描述符的128维向量。

下面函数用于从上面输出文件中,将特征读取到Numpy数组中的函数。

def read_features_from_file(filename):
    """读取特征值属性值,然后将其以矩阵形式返回"""
    
    f = loadtxt(filename)
    return f[:,:4],f[:,4:] # 特征位置,描述子

读取特征后,通过在图像上绘制出它们的位置,可以将其可视化。下面的函数可以实现这个功能:

def plot_features(im, locs, circle=False):
    """显示带有特征的图像
        输入:im(数组图像),locs(每个特征的行、列、尺度和方向角度)"""

    def draw_circle(c,r):
        t = arange(0,1.01,.01)*2*pi
        x = r*cos(t) + c[0]
        y = r*sin(t) + c[1]
        plot(x,y,'b',linewidth=2)

    imshow(im)
    if circle:
        for p in locs:
            draw_circle(p[:2],p[2])
    else:
        plot(locs[:,0],locs[:,1],'ob')
    axis('off')
    return

我们通过下面的命令绘制SIFT特征位置的图像:

	imname = 'D:\\123\图像处理\Image Processing\Image Processing\mona_target.jpg'
    im1 = array(Image.open(imname).convert('L'))
    process_image(imname,'mona.sift')
    l1,d1 = read_features_from_file('mona.sift')

    figure()
    gray()
    plot_features(im1,l1,circle=True)
    show()

在这里插入图片描述

2.4 匹配描述子

对于将一幅图像中的特征匹配到另一幅图像的特征,一种稳健的准则(同样是由Lowe提出的)是使用者两个特征距离和两个最匹配特征距离的比率。相比于图像中的其他特征,该准则保证能够找到足够相似的唯一特征。使用该方法可以使错误的匹配数降低。下面的代码实现了匹配函数。

def match(desc1, desc2):
    """对于第一幅图像的每个描述子,选取其在第二幅图像中的匹配
        输入:desc1(第一幅图像中的描述子),desc2(第二幅图像中的描述子)"""

    desc1 = array([d/linalg.norm(d) for d in desc1])
    desc2 = array([d/linalg.norm(d) for d in desc2])

    dist_ratio = 0.6
    desc1_size = desc1.shape

    matchscores = zeros((desc1_size[0],1), 'int')
    desc2t = desc2.T    #预先计算矩阵转置
    for i in range(desc1_size[0]):
        dotprods = dot(desc1[i,:], desc2t) #向量点乘
        dotprods = 0.9999*dotprods
        # 反余弦和反排序,返回第二幅图像中特征的索引
        index = argsort(arccos(dotprods))

        # 检查最近邻的角度是否小于dist_ratio乘以第二近邻的角度
        if arccos(dotprods)[index[0]] < dist_ratio * arccos(dotprods)[index[1]]:
            matchscores[i] = int(index[0])

    return matchscores

def match_twosided(desc1,decs2):
    """双向对称版本的match"""

    matches_12 = match(desc1, decs2)
    matches_21 = match(decs2, decs2)

    ndx_12 = matches_12.nonzero()[0]

    # 去除不对称匹配
    for n in ndx_12:

        if matches_21[int(matches_12[n])] != n:
            matches_12[n] = 0

    return matches_12

def appendimages(im1, im2):
    """返回将两幅图像并排拼接成的一幅新图像"""

    # 选取具有最少行数的图像,然后填充足够的空行
    row1 = im1.shape[0]
    row2 = im2.shape[0]

    if row1 < row2:
        im1 = concatenate((im1,zeros((row2-row1,im1.shape[1]))), axis=0)
    elif row1 > row2:
        im2 = concatenate((im2,zeros((row1-row2,im2.shape[1]))), axis=0)

    # 如果这些情况都没有,那么他们的行数相同,不需要进行填充

    return concatenate((im1,im2), axis=1)

def plot_matches(im1, im2, locs1, locs2, matchscores, show_below=True):
    """显示一幅带有连接匹配之间连线的图片
        输入:im1,im2(数组图像),locs1,locs2(特征位置),matchscores(match的输出),
        show_below(如果图像应该显示再匹配下方)"""

    im3 = appendimages(im1,im2)
    if show_below:
        im3 = vstack((im3,im3))

    imshow(im3)

    cols1 = im1.shape[1]
    for i in range(len(matchscores)):
        if matchscores[i] > 0:
            plot([locs1[i, 0], locs2[matchscores[i, 0], 0] + cols1], [locs1[i, 1], locs2[matchscores[i, 0], 1]], 'c')
    axis('off')

在这里插入图片描述
在这里插入图片描述
这里好像是我用的素材图片太小了,最后特征点没有连接,从上图观察一下可知,左图中对人脸刻画的特征点极少,大部分都是边框,右边对人脸细节刻画较多,所以在最后一步没有匹配的特征点,感兴趣可以换一些大的图片去做sift算法匹配。

猜你喜欢

转载自blog.csdn.net/weixin_42939683/article/details/107508068