OpenCV算法學(xué)習(xí)筆記之邊緣檢測(cè)(二)

此系列的其他文章:
OpenCV算法學(xué)習(xí)筆記之初識(shí)OpenCV
OpenCV算法學(xué)習(xí)筆記之幾何變換
OpenCV算法學(xué)習(xí)筆記之對(duì)比度增強(qiáng)
OpenCV算法學(xué)習(xí)筆記之平滑算法
OpenCV算法學(xué)習(xí)筆記之閾值分割
OpenCV算法學(xué)習(xí)筆記之形態(tài)學(xué)處理
OpenCV算法學(xué)習(xí)筆記之邊緣檢測(cè)(一)
OpenCV算法學(xué)習(xí)筆記之形狀檢測(cè)

更多文章可以訪問(wèn)我的博客Aengus | Blog

Canny邊緣檢測(cè)

原理

基于卷積運(yùn)算的邊緣檢測(cè)算法(即上一篇OpenCV筆記中提到的),有以下兩個(gè)缺點(diǎn):

(1)沒(méi)有充分利用邊緣的梯度方向;

(2)最后輸出的邊緣二值圖,只是簡(jiǎn)單地利用閾值進(jìn)行處理,邊緣信息損失程度和閾值相關(guān);

Canny算法基于這兩點(diǎn)做出了改進(jìn),主要為:

(1)基于邊緣梯度方向的非極大值抑制;

(2)雙閾值的滯后閾值處理;

Canny算法的步驟近似如下:

第一步:圖像矩陣I分別與水平方向上的卷積核sobel_x和垂直方向上的卷積核sobel_y卷積得到dxdy,然后利用平方和的開(kāi)方magnitude = \sqrt{dx^2 + dy^2}得到邊緣強(qiáng)度,這一步和Sobel邊緣檢測(cè)一樣,也可以用Prewitt核代替。邊界的處理方式是補(bǔ)零。

假設(shè)有以下矩陣:

例矩陣

與sobel算子進(jìn)行卷積后,得到dxdy

dx與dy

然后計(jì)算平方和并開(kāi)方,得到邊緣強(qiáng)度(梯度強(qiáng)度),這里只寫(xiě)整數(shù)部分:

梯度矩陣

Sobel邊緣檢測(cè)是對(duì)magnitude的結(jié)果大于255的值截?cái)酁?55,然后轉(zhuǎn)換為8位圖就得到了邊緣強(qiáng)度圖的灰度顯示。而Canny算法的處理方式與此不同。

第二步:利用第一步得到的dxdy,計(jì)算出梯度方向angle=arctan2(dy,dx),即對(duì)每一個(gè)位置(r,c),angle(r,c) = arctan2(dy(r,c), dx(r,c))代表該位置的梯度方向,一般用角度表示,即angle(r,c) \in [0, 180] \cup [-180, 0]??梢岳肞ython的函數(shù)包math中的atan2進(jìn)行求取,也可以利用Numpy的函數(shù)arctan2,代碼如下:

y = np.array([[1, 1], [-1, -1]])
x = np.array([[1, -1], [-1, 1]])
angle = np.arctan2(y, x)/np.pi*180

結(jié)果為:

array([[45., 135.],
       [-135., -45.]])

用第一步得到的dxdy,計(jì)算得到每一個(gè)位置的梯度角:

梯度角

第三步:對(duì)每一個(gè)位置進(jìn)行非極大值抑制處理,非極大值抑制處理操作操作返回的仍然是一個(gè)矩陣,假設(shè)為nonMaxSup。在上面對(duì)示例中,邊界的處理是補(bǔ)零,導(dǎo)致所得到對(duì)magnitude產(chǎn)生額外的邊緣響應(yīng);如果采用對(duì)是以邊界為對(duì)稱(chēng)的邊界擴(kuò)充方式,那么卷積結(jié)果的邊界全是0。在非極大值抑制這一步中,對(duì)邊界不進(jìn)行任何處理。

所謂對(duì)非極大值抑制,就是如果magnitude(r,c)在沿著梯度方向angle(r,c)上的鄰域內(nèi)是最大的則將nonMaxSup(r,c)設(shè)置為最大值,否則將其設(shè)置為0。

首先將nonMaxSup初始化為以下矩陣:

初始化

接下來(lái)以填充nonMaxSup(1,1)為例:首先在magnitude(1,1)上放置倒置對(duì)坐標(biāo)軸,然后對(duì)應(yīng)到angle,發(fā)現(xiàn)angle(1,1)=133,再按照該梯度方向畫(huà)出直線,最后以magnitude(1,1)為中心對(duì)3*3鄰域內(nèi),大體定位出梯度方向上對(duì)領(lǐng)域,即右上方和左下方。如下圖所示:

magnitude(1,1)

由于912>292912>276,故令nonManSup(1,1)=magnitude(1,1)。

一般可以將梯度離散為以下幾種情況:

  • angle(r,c) \in [0,22.5)\cup (-22.5,0] \cup (157.5,180] \cup (-180, 157.5);
  • angle(r,c)\in [22.5, 67.5) \cup [-157.5, -112.5)
  • angle(r,c)\in [67.5, 112.5] \cup [-112.5,-67.5];
  • angle(r,c)\in (112.5, 157.5] \cup [-67.5,-22.5];

對(duì)應(yīng)的領(lǐng)域如下所示:

對(duì)應(yīng)情況

第四步:雙閾值對(duì)滯后閾值處理。經(jīng)非極大值抑制后得到的邊緣強(qiáng)度圖一般需要閾值化處理,常用對(duì)是全局閾值分割和局部自適應(yīng)閾值分割。還有另外一種方法:滯后閾值處理,它使用高閾值和低閾值,按照下列規(guī)則進(jìn)行閾值化處理:

  1. 邊緣強(qiáng)度大于高閾值對(duì)那些點(diǎn)作為確定邊緣點(diǎn);
  2. 邊緣強(qiáng)度比低閾值小的那些點(diǎn)立即被剔除;
  3. 處于高低閾值之間對(duì)那些點(diǎn),只有這些點(diǎn)能按某一路徑與確認(rèn)邊緣點(diǎn)相連時(shí),才可以作為邊緣點(diǎn)被接受。組成這一路徑的所有點(diǎn)都要比低閾值大。實(shí)際中,可以先選定邊緣強(qiáng)度大于高閾值的所有確定邊緣點(diǎn),然后在邊緣強(qiáng)度大于低閾值的情況下盡可能的延長(zhǎng)邊緣;

Python實(shí)現(xiàn)

最好利用dx與dy的平方和開(kāi)方對(duì)形式來(lái)衡量邊緣強(qiáng)度。

def non_maximum_suppression(dx, dy):
    """
    非極大值抑制
    :param dx: 與水平卷積核卷積運(yùn)算后的結(jié)果
    :param dy: 與垂直卷積核卷積運(yùn)算后對(duì)結(jié)果
    :return 非極大值抑制后的矩陣
    """
    # 邊緣強(qiáng)度
    edge_mag = np.sqrt(np.power(dx, 2.0) + np.power(dy, 2.0))
    # 高、寬
    rows, cols = dx.shape
    # 梯度方向
    grad_direction = np.zeros(dx.shape)
    # 邊緣強(qiáng)度的非極大值抑制
    edge_mag_non_max_sup = np.zeros(dx.shape)
    for r in range(1, rows-1):
        for c in range(1, cols-1):
            # angle的范圍[0, 180],[-180, 0]
            angle = math.atan2(dy[r][c], dx[r][c]) / math.pi*180
            grad_direction[r][c] = angle
            # 左右方向
            if abs(angle) < 22.5 or abs(angle) > 157.5:
                if edge_mag[r][c] > edge_mag[r][c-1] and edge_mag[r][c] > edge_mag[r][c+1]:
                    edge_mag_non_max_sup[r][c] = edge_mag[r][c]
            # 左上右下方向
            if (abs(angle) >= 22.5 or abs(angle) < 67.5) or (-angle > 112.5 and -angle <= 157.5):
                if edge_mag[r][c] > edge_mag[r-1][c-1] and edge_mag[r][c] > edge_mag[r+1][c+1]:
                    edge_mag_non_max_sup[r][c] = edge_mag[r][c]
            # 上下方向
            if abs(angle) >= 67.5 and abs(angle) <= 112.5:
                if edge_mag[r][c] > edge_mag[r-1][c] and edge_mag[r][c] > edge_mag[r+1][c]:
                    edge_mag_non_max_sup[r][c] = edge_mag[r][c]
            # 右上左下方向
            if (abs(angle) >= 112.5 or abs(angle) <= 157.5) or (-angle >= 22.5 and -angle < 67.5):
                if edge_mag[r][c] > edge_mag[r-1][c+1] and edge_mag[r][c] > edge_mag[r+1][c-1]:
                    edge_mag_non_max_sup[r][c] = edge_mag[r][c]
    return edge_mag_non_max_sup

接下來(lái)是滯后閾值處理:

def check_in_range(r, c, rows, cols):
    """
    判斷一個(gè)點(diǎn)的坐標(biāo)是否在圖像范圍內(nèi)
    """
    if (r >= 0 and r < rows) and (c >= 0 and c < cols):
        return True
    else:
        return False


def trace(edge_mag_non_max_sup, edge, lower_thresh, r, c, rows, cols):
    """
    滯后閾值處理對(duì)第四步:延長(zhǎng)邊緣
    """
    # 大于高閾值的點(diǎn)為確定邊緣點(diǎn)
    if edge[r][c] == 0:
        edge[r][c] = 255
        for i in range(-1, 2):
            for j in range(-1, 2):
                if check_in_range(r+i, c+j, rows, cols) and edge_mag_non_max_sup[r+i][c+j] >= lower_thresh:
                    tarce(edge_mag_non_max_sup, edge, lower_thresh, r+i, c+j, rows, cols)
                    
                    
def hysteresis_threshold(edge_non_max_sup, lower_thresh, upper_thresh):
    """
    滯后閾值處理
    :param edge_non_max_sup: 待處理邊緣強(qiáng)度圖
    :param lower_thresh: 低閾值
    :param upper_thresh: 高閾值
    :return 處理后對(duì)邊緣強(qiáng)度圖
    """
    # 高寬
    rows, cols = edge_non_max_sup.shape
    edge = np.zeros(edge_non_max_sup.shape, np.uint8)
    for r in range(1, rows-1):
        for c in range(1, cols-1):
            # 大于高閾值的點(diǎn)為確定邊緣點(diǎn),而且以該點(diǎn)為起始點(diǎn)延長(zhǎng)邊緣
            if edge_non_max_sup[r][c] >= upper_thresh:
                trace(edge_non_max_sup, edge, lower_thresh, r, c, rows, cols)
            # 小于低閾值的被剔除
            if edge_non_max_sup[r][c] < lower_thresh:
                edge[r][c] = 0
    return edge

OpenCV提供函數(shù)void Canny(InputArray image, OutputArray edges, double threshold1, double threshold2, int apertureSize=3, bool L2gradient=false),其中threshold1代表低閾值,threshold2代表高閾值,apertureSize代表Sobel核的窗口尺寸,L2gradient代表計(jì)算邊緣強(qiáng)度時(shí)使用的方式,值等于true時(shí)用的是平方和形式,否則使用的是絕對(duì)值和的形式。

Laplacian邊緣檢測(cè)

原理

二維的拉普拉斯變換由以下公式定義:
\begin{align}\nabla^2f(x,y) &= \frac{\partial^2f(x,y)}{\partial x^2} + \frac{\partial^2f(x,y)}{\partial y^2}\\ &\approx \frac{\partial (f(x+1,y) - f(x,y))}{\partial x} + \frac{\partial (f(x,y+1) - f(x,y))}{\partial y} \\ &\approx f(x+1,y) + f(x-1,y)+f(x,y-1)+f(x,y+1)-4f(x,y) \end{align}
推廣到二維矩陣,即矩陣進(jìn)行拉普拉斯變換,也就是與以下卷積核進(jìn)行卷積(這兩種都可以):
l_0 = \left( \begin{matrix} 0&-1&0 \\ -1&4&-1 \\ 0 &-1&0 \end{matrix}\right) , l_{0^-} = \left( \begin{matrix} 0&1&0 \\ 1&-4&1 \\ 0 &1&0 \end{matrix}\right)
錨點(diǎn)在中心位置。圖像與拉普拉斯核進(jìn)行卷積運(yùn)算的本質(zhì)是計(jì)算任意位置的值與其在水平方向和垂直方向上四個(gè)相鄰點(diǎn)平均值之間的差值(只是相差一個(gè)4的倍數(shù))。

拉普拉斯邊緣檢測(cè)沒(méi)有進(jìn)行平滑處理,所以對(duì)噪點(diǎn)比較敏感;優(yōu)點(diǎn)是只有一個(gè)卷積核,運(yùn)算量比較低。

拉普拉斯核還有以下幾種形式:
l_1 = \left( \begin{matrix} -1&-1&-1 \\ -1&8&-1 \\ -1&-1&-1 \end{matrix}\right) , l_2 = \left( \begin{matrix} 2&-1&2 \\ -1&-4&-1 \\ 2&-1&2 \end{matrix}\right)
l_3 = \left( \begin{matrix} 0&2&0 \\ 2&8&2 \\ 0&2&0 \end{matrix}\right), l_4 = \left( \begin{matrix} 2&0&2 \\ 0&-8&0 \\ 2&0&2 \end{matrix}\right)
拉普拉斯核內(nèi)所有元素的和必須為0,這樣就使得在恒等灰度值區(qū)域不會(huì)產(chǎn)生錯(cuò)誤的邊緣。上述的拉普拉斯核均是不可分離的。

Python實(shí)現(xiàn)

def laplacian(image, _boundary='fill', _fillvalue=0):
    # 第一步:圖像與拉普拉斯核卷積
    laplacian_kernel = np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]], np.float32)
    # 卷積
    i_conv_lap = signal.convolve2d(image, laplacian_kernel, mode='same', boundary=_boundary, fillvalue=_fillvalue)
    # 第二步:進(jìn)行閾值化處理
    i_conv_lap[i_conv_lap > 0] = 255
    i_conv_lap[i_conv_lap <= 0] = 0
    i_conv_lap.astype(np.uint8)
    return i_conv_lap

在閾值化處理時(shí),也可以用以下公式進(jìn)行處理以得到水墨畫(huà)效果
\begin{equation} abstraction(r,c) = \left\{ \begin{array}{lcl} 255,\quad i\_conv\_lap(r,c) > 0 \\ 0, \quad \quad i\_conv\_lap(r,c) \leq 0 \end{array} \right. \end{equation}
OpenCV提供函數(shù)void Laplacian(InputArray src, OutputArray dst, int ddepth, int ksize=-1, double scale=1, double delta=0, int borderType=BORDER_DEFAULT)實(shí)現(xiàn)拉普拉斯變換,參數(shù)解釋如下所示:

參數(shù) 解釋
src 輸入矩陣
dst 輸出矩陣
ddepth 輸出矩陣的數(shù)據(jù)類(lèi)型(位深)
ksize 拉普拉斯核的類(lèi)型
scale 比例系數(shù)
delta 平移系數(shù)
borderType 邊界擴(kuò)充類(lèi)型

參數(shù)ksize等于1時(shí)采用的是l_{0^-}形式的拉普拉斯核,等于3時(shí)采用的是l_4形式的核。

高斯拉普拉斯(LoG)邊緣檢測(cè)

由于拉普拉斯變換對(duì)于噪點(diǎn)比較敏感,在進(jìn)行變換前先進(jìn)行高斯平滑再進(jìn)行卷積,這就是LoG邊緣檢測(cè)。雖然平滑和變換需要兩次卷積運(yùn)算,但是這里我們可以利用一次卷積運(yùn)算即可完成。

原理

二維高斯函數(shù)為
gauss(x,y,\sigma)=\frac{1}{2\pi \sigma^2}\exp(-\frac{(x^2+y^2)}{2\sigma^2})
其拉普拉斯變化如下(省略化簡(jiǎn)過(guò)程):
\begin{align}\nabla^2(gauss(x,y,\sigma)) &= \frac{\partial^2(gauss(x,y,\sigma))}{\partial x^2} + \frac{\partial^2(gauss(x,y,\sigma))}{\partial y^2}\\ &= \frac{1}{2\pi \sigma^4}(\frac{x^2+y^2}{\sigma^2} - 2)\exp(-\frac{x^2+y^2}{2\sigma^2}) \end{align}
其步驟如下:

第一步:構(gòu)建H×W、標(biāo)準(zhǔn)差為\sigma的LoG卷積核:
LoG_{H×W}=[\nabla^2gauss(w-\frac{W-1}{2}, h-\frac{H-1}{2}, \sigma)]_{0 \leq h <H,0\leq w<W}
其中H,W均為奇數(shù),且一般H=W,錨點(diǎn)位置在(\frac{H-1}{2}, \frac{W-1}{2})。

第二步:圖像與LoG_{H×W}進(jìn)行卷積,結(jié)果記為i_cov_log;

第三步:對(duì)i_cov_log進(jìn)行二值化處理,閾值為0;

由于
\nabla^2(gauss(x,y,\sigma))=\frac{1}{\sigma^2}[(\frac{x^2}{\sigma^2} - 1)gauss(x,\sigma)]gauss(y,\sigma)+\frac{1}{\sigma^2}[(\frac{y^2}{\sigma^2} - 1)gauss(y,\sigma)]gauss(x,\sigma)
故可以將高斯拉普拉斯核分離為兩個(gè)卷積核,運(yùn)算時(shí),圖像先與水平方向核卷積,再與垂直卷積核卷積得到結(jié)果1;然后圖像先與垂直卷積核卷積,再與水平卷積核卷積得到結(jié)果2,結(jié)果1與結(jié)果2相加再進(jìn)行閾值化處理得到最終結(jié)果。

Python實(shí)現(xiàn)

這里給出非分離的邊緣檢測(cè),可以把\nabla^2(gauss(x,y,\sigma))前面的系數(shù)\frac{1}{2\pi \sigma^4}去掉。

def create_LoG_kernel(sigma, size):
    h, w = size
    r, c = np.mgrid[0:h:1, 0:w:1]
    r -= (h-1) / 2
    c -= (w-1) / 2
    # 方差
    sigma2 = pow(sigma, 2.0)
    # 高斯拉普拉斯核
    norm2 = np.power(r, 2.0) + np.power(c, 2.0)
    log_kernel = (norm2/sigma2 - 2) * np.exp(-norm2/(2*sigma2))
    return log_kernel


def LoG(image, sigma, size, _boundary='symm'):
    """
    :param _boundary: symm代表反射擴(kuò)充
    """
    log_kernel = create_LoG_kernel(sigma, size)
    # 卷積
    img_conv_log = signal.convolve2d(image, log_kernel, 'same', boundary=_boundary)
    return img_conv_log

對(duì)于高斯拉普拉斯核的尺寸,一般取(6*\sigma+1)×(6*\sigma+1),即大于6\sigma的最小奇數(shù),這樣得到的邊緣效果會(huì)比較好。隨著尺度(標(biāo)準(zhǔn)差)的增大,得到的邊緣尺度也越來(lái)越大,越來(lái)越失去邊緣的細(xì)節(jié)。

高斯差分(DoG)邊緣檢測(cè)

可以用差分代替卷積來(lái)近似高斯拉普拉斯變換以減小計(jì)算量。

原理

由于
\sigma \nabla^2(gauss(x,y,\sigma)) = \frac{\partial gauss(x,y,\sigma)}{\partial \sigma}
以及一階導(dǎo)數(shù)的定義:
\begin{align}\frac{\partial gauss(x,y,\sigma)}{\partial \sigma} &= \lim_{k \to 1}\frac{gauss(x,y,k*\sigma) - gauss(x,y,\sigma)}{k*\sigma - \sigma} \\ &\approx \frac{gauss(x,y,k*\sigma) - gauss(x,y,\sigma)}{k*\sigma - \sigma} \end{align}
可以得到高斯拉普拉斯的近似值:
\nabla^2 gauss(x,y,\sigma) = \frac{gauss(x,y,k*\sigma) - gauss(x,y,\sigma)}{\sigma^2 (k-1)}
該近似值常稱(chēng)為高斯差分。

步驟如下:

第一步:構(gòu)建H×W、標(biāo)準(zhǔn)差為\sigma的DoG卷積核:
DoG_{H×W}=[DoG(w-\frac{W-1}{2}, h-\frac{H-1}{2}, \sigma)]_{0 \leq h <H,0\leq w<W}
其中H,W均為奇數(shù),且一般H=W,錨點(diǎn)位置在(\frac{H-1}{2}, \frac{W-1}{2})。

第二步:圖像與DoG_{H×W}進(jìn)行卷積,結(jié)果記為i_cov_dog;

第三步:對(duì)i_cov_dog進(jìn)行二值化處理,閾值為0;

參考

《OpenCV算法精解——基于Python和C++》(張平)第八章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容