圖像處理--歸一化切割--(normalized cut)--Python實現

歸一化切割Normalized cut 是一種分羣(cluster grouping)技術,在數據處理和圖像處理方面有很廣的運用

用其實現圖像分割的思路是,把一個圖片看成一個圖(graph), 然後計算權重圖(weighted graph),然後分割成一些具有相同特徵(紋理, 顏色,明度等)的區域。

想要理解代碼的含義,首先要先要理解一些基本的概念:

1.什麼是圖(graph)?

我們定義G = (V, E), 其中,V是頂點(vertex), E是邊(edge)。

2.什麼是權重圖(weighted graph)

就是邊(edge)上有權重(weight)的圖,如下圖所示:


                               (圖1)               

我們把這個圖分成兩個子集左側的A和右側的B, 通過觀察,A由4個頂點組成,B由5個頂點組成。需要注意的是,同屬於一個子集中的頂點之間的邊的權重的值相對來說會大一些。子集與子集直接的點的邊上的權重會小, 這個特性也就是normalized cut能分割區域的基礎了。這張圖表示是,相近的點之間的權重大。但是實際運用中,距離並不是衡量權重的唯一標準。如何計算權重是具體項目具體設計的了。

想要理解normalized cut 需要先理解什麼是分割(cut)與最小化分割(min cut)

我這裏用相對數學一點的表述方式:

把G = (V,E) 分成兩個子集A,B,另:



其中,  就是權重(weight), 最小化分割就是讓上式的值最小的分割。如何理解這個過程?

拿圖一做例子,我們把圖一看成一個整體G,目的是把它分成兩個部分。顯然中間的兩條權重爲0.1的邊就是最小化切割。最小化分割很完美的就解決了把這個G分成兩部分的任務,但是問題來了如下圖(圖2)所示,最小化切割性能就不很好了。


                    (圖2)

可以看到,圖2所示的情況,想要的結果是中間虛線表示的分割,但是最小化切割卻切掉了最邊緣的角。這很容易理解,因爲最小化切割就是讓cut(A,B)的值最小的情況,而邊緣處cut值確實是最小。因此我們稱最小化切割時有偏差的(bias)。如何去除這種偏差就要引入我們這篇文章的主角歸一化切割了。


其中:


assoc(A,V)的含義是A中所有點到圖中做有點的權重的和。通過上面的公式我們可以很清晰的看到Ncut在追求不同子集間點的權重最小值的同時也追求同一子集間點的權重最大值

至此,數學含義已經講的差不多了,但是如果想要寫成代碼,我們需要用矩陣數學的知識重新把normalized cut解釋一遍。

受限我們要定義幾個概念:

1) x 表示一個 N=|V|維的指標向量, x_i = 1 如果i頂點在子集A中,x_i = -1 如果頂點i在子集B中

2) , d(i) 的含義就是第i個頂點與其他所有頂點的邊的權重之和。

3)設是一個[N,N]維的對角矩陣,d(i) (i從1到N)爲對角線元素

4)設是一個[N,N]維的對稱矩陣,W(i,j) = ,所以就是用來描述點與點之間邊的權重的矩陣

5)需要聲明的是,在矩陣運算中表示[N,1]維度的全一矩陣

通過種種計算(推導過程參考https://people.eecs.berkeley.edu/~malik/papers/SM-ncut.pdf論文原文pdf文件第三頁)[1]

我們的任務就是求解一下特徵值問題:


其中y就是特徵向量,lambda就是特徵值了。我們要的分割點就是第二小特徵值所對應的特徵向量。

計算這個特徵值問題的複雜度是, 不過scipy.sparse.linalg.eig()函數所用的Lanczos算法會將複雜度降到O(n)(畢竟D-W是個稀疏矩陣)

爲了利用稀疏矩陣我們需要構建:

例子:

分割圖3所示圖片,50px X 50px


                    (圖3)

這張彩色RGB圖片,我們想要分割,首先設每一個圖片中的像素點都是圖(graph)中的一個頂點(vertex),然後計算每個點與點之間的權重:


其中X(i)表示頂點i在圖片中的座標,X是一個[N,1,2]的矩陣,在3維存儲着像素點的縱橫座標值。對於灰度圖來說F(i)就是i點所在的明度值,F是一個[N,1]的矩陣,對於RGB圖片來說F(i) = [B,G,R], F是一個[N,1,3]的矩陣,在3維存着RGB的值。

我們看一下結果:

1)直接求特徵值:耗時3分鐘


2) 利用Lanczos方法求稀疏矩陣的特徵值耗時3秒


雖然看起來怪怪的,但是後續很容易就能提取出來了(加上濾波二值化之類的等等)。

我這裏只是做了二分,想要將圖片分成很多部分,需要對剩下的部分迭代幾次,這一部分就很簡單了。

下面是我寫的代碼,,大家參考一下。

'''
reference:
[1] J. Shi and J. Malik, “Normalized cuts and image segmentation”, 
    IEEE Trans. on Pattern Analysisand Machine Intelligence, Vol 22

[2] https://github.com/SatyabratSrikumar/Normalized-Cuts-and-Image-Segmentation-Matlab-Implementation

edited by JY Wang @ 2018-6-3 SYR Unv
'''

import cv2
import numpy as np
from scipy.linalg.decomp import eig
from scipy import sparse
from scipy.sparse.linalg import eigs

class Ncut(object):
'''
This class is write for RGB image, so if you want to processing grayscale, some adjustment should worked on 
F_maker, W_maker function :)
'''

    def __init__(self, img):
        '''
        :param img: better no larger than 300px,300px
        '''
        self.no_rows, self.no_cols, self.channel = img.shape
        self.N = self.no_rows * self.no_cols
        self.V_nodes = self.V_node_maker(img)
        self.X = self.X_maker()
        self.F = self.F_maker(img)
        # parameter for W clculate
        self.r = 2
        self.sigma_I = 4
        self.sigma_X = 6
        # Dense W,D
        self.W = self.W_maker()
        self.D = self.D_maker()

    # V_nodes shape : [self.N,1,3]
    def V_node_maker(self, img):
        b,g,r = cv2.split(img)
        b = b.flatten()
        g = g.flatten()
        r = r.flatten()
        V_nodes = np.vstack((b,g))
        V_nodes = np.vstack((V_nodes,r))
        return V_nodes

    def X_maker(self):
        X_temp = np.arange(self.N)
        X_temp = X_temp.reshape((self.no_rows,self.no_cols))
        X_temp_rows = X_temp // self.no_rows
        X_temp_cols = (X_temp // self.no_cols).T
        X = np.zeros((self.N, 1, 2))
        X[:,:,0] = X_temp_rows.reshape(self.N,1)
        X[:,:,1] = X_temp_cols.reshape(self.N,1)
        return X

    def F_maker(self,img):
        if self.channel < 2:
            return self.gray_feature_maker(img)
        else:
            return self.color_img_feature_maker(img)

    def gray_feature_maker(self,img):
        print('need to ')

    def color_img_feature_maker(self,img):
        F = img.flatten().reshape((self.N,1,self.channel))
        F = F.astype('uint8')
        return F

    def W_maker(self):
        X = self.X.repeat(self.N,axis = 1)
        X_T = self.X.reshape((1,self.N,2)).repeat(self.N,axis = 0)
        diff_X = X - X_T
        diff_X = diff_X[:,:,0]**2 + diff_X[:,:,1]**2

        F = self.F.repeat(self.N,axis = 1)
        F_T = self.F.reshape((1,self.N,3)).repeat(self.N,axis = 0)
        diff_F = F - F_T
        diff_F = diff_F[:,:,0]**2 + diff_F[:,:,1]**2 + diff_F[:,:,2]**2

        W_map = diff_X < self.r**2 # valid map for W

        W = np.exp(-((diff_F / (self.sigma_I**2)) + (diff_X / (self.sigma_X**2))))

        return W * W_map 

    def D_maker(self):
        # D is a diagonal matrix using di as diagonal, di is the sum of weight of node i with all other nodes
        d_i = np.sum(self.W, axis=1)
        D = np.diag(d_i)
        return D

    def EigenSolver(self):
        L = self.D - self.W
        R = self.D
        lam,y = eig(L, R)
        index = np.argsort(lam)

        top2 = lam[index[:2]].real
        smallest_2 = y[:,index[1]]
        print('dense eigenvector')
        return smallest_2.real

    def EigenSolver_sparse(self):
        s_D = sparse.csr_matrix(self.D)
        s_W = sparse.csr_matrix(self.W)
        s_D_nhalf = np.sqrt(s_D).power(-1)
        L = s_D_nhalf @ (s_D - s_W) @ s_D_nhalf
        lam,y = eigs(L)
        index = np.argsort(lam)

        top2 = lam[index[:2]].real
        smallest_2 = y[:, index[1]]
        print('sparse eigenvector')
        return smallest_2.real


if __name__ == '__main__':
    # This is dense eigenvector method
    # img = cv2.imread('picture/Ncut_test.png', cv2.IMREAD_COLOR)
    # cutter = Ncut(img)
    # eigenvector = cutter.EigenSolver()

    # the process is cost too much time, so I saved the results in a txt file, just ignore this part if you need't
    
    # file = open('result.txt','w')
    # for i in eigenvector:
    #     file.write(str(i))
    #     file.write(',')
    # file = open('result.txt', 'r')
    # a = file.read()
    # b = np.array(a.split(','))

    # This is sparse eigenvector method
    img = cv2.imread('picture/Ncut_test.png', cv2.IMREAD_COLOR)
    cutter = Ncut(img)
    eigenvector = cutter.EigenSolver_sparse()
    b = eigenvector
    b = b.reshape((50,50)).astype('float64')
    b = (b/b.max())*255
    cv2.imshow('eigvec',b.astype('uint8'))
    cv2.waitKey()
    print('Finished!')
參考文獻:

[1] J. Shi and J. Malik, “Normalized cuts and image segmentation”, IEEE Trans. on Pattern Analysisand Machine Intelligence, Vol 22, No. 8, Aug., 2000.

[2] Jitendra Malik, Jianbo Shi UC Berkeley,Normalized-Cuts-and-Image-Segmentation github地址

[3] Haining Wang, ECS 231 Final Project, Image Segmentation by Normalized Cut,University of California, Davis


轉發請標明出處

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章