キャニー エッジ検出アルゴリズム (Python 実装)


最適マージン基準

    Canny の目標は、最適なエッジ検出アルゴリズムを見つけることです. 最適なエッジ検出の意味は次のとおりです:
    (1) 最適な検出: アルゴリズムは、画像内の実際のエッジを可能な限り多く識別することができます, 実際のエッジが欠落している確率とエラー非エッジを検出する確率は可能な限り小さい
    (2) 最適な位置決め基準: 検出されたエッジ ポイントの位置が実際のエッジ ポイントの位置に最も近いか、または検出されたエッジが実際のエッジ ポイントから逸脱する度合いノイズの影響によるオブジェクトの実際のエッジ最小;
    (3) 検出点とエッジ点の 1 対 1 の対応: 操作者によって検出されたエッジ点と実際のエッジ点は 1 対 1 で対応する必要があります。エッジポイント。


アルゴリズムの実装手順

    キャニー エッジ検出アルゴリズムは、次の 5 つのステップに分けることができます。

1. ガウス フィルタを適用して画像を滑らかに (ぼかし)、ノイズを除去します。

    ガウス フィルターは、ガウス関数を離散化し、フィルター内の対応する水平および垂直座標インデックスをガウス関数に代入して、対応する値を取得します。

    2 次元ガウス関数は次のとおりです。ここで、(x , y) は座標、σ は標準偏差
H ( x , y ) = 1 2 π σ 2 e − x 2 + y 2 2 σ 2 (1) H(x,y ) = \frac{1}{2\pi σ^2} e^{- \frac{x^2 + y^2}{2σ^2}} \tag1H ( x ,=2p_ _ _21e2p _2バツ2 +y2( 1 )
    異なるサイズのフィルターは異なる値を持ちます. (2k+1)x(2k+1) フィルターの計算式は次のとおりです:
H [ i , j ] = 1 2 π σ 2 e − ( i − k − 1 ) 2 + ( j − k − 1 ) 2 2 σ 2 (2) H[i,j] = \frac{1}{2\pi σ^2} e^{- \frac{(ik -1) ^2 + (jk-1)^2}{2σ^2}} \tag2H [ i ,j ]=2p_ _ _21e2p _2( i k 1 )2 +(jk1)2( 2 )
    一般的なガウス フィルターのサイズは 5*5、σ = 1.4 で、その近似値は次のとおりです。


2.勾配の強さと方向を計算する

    次に、エッジ、つまりグレースケール強度が最も強く変化する位置を探す必要があります (黒エッジと白エッジの中間がエッジであり、そのグレースケール値が最も変化します)。画像では、グラデーションを使用してグレー値の変化の度合いと方向を表しています。
    一般的な方法では、ソーベル フィルター [水平方向 x および垂直方向 y 方向] を使用して勾配と方向を計算します。水平方向の
ソーベル演算子 G x : y 方向のエッジを検出するために使用されます。

-1 0 1
-2 0 2
-1 0 1

    垂直方向のソーベル演算子 G y : x 方向のエッジを検出するために使用されます (エッジ方向は勾配方向に垂直です)。

1 2 1
0 0 0
-1 -2 -1

    勾配と方向は、次の式を使用して計算されます:
G = ( G x 2 + G y 2 ) (3) G = \sqrt{(G_x^2 + G_y^2)} \tag3G=( Gバツ2+Gy2) ( 3 )
θ = arctan G および G x (4) \theta = arctan {\frac{G_y}{G_x}} \tag4=アークタン_ _ _ _ _G×Gy( 4 )


3. 非最大抑制技術 NMS を適用して、エッジの誤検出を排除する

原理: 勾配マトリックス上のすべての点をトラバースし、最大値を持つピクセルをエッジ方向に保持します

    このステップの目的は、ぼやけた(ぼやけた)境界を明確(シャープ)にすることです。簡単に言えば、各ピクセルのグラデーション強度の最大値を保持し、他の値を削除することです。各ピクセルに対して、次の操作を実行します。a
    ) グラデーションの方向を次の値 (0、45、90、135、180、225、270、315) (つまり、上、下、左、右、および 45 度の方向)

    b) 勾配方向の正と負の方向で、ピクセル点とピクセル点の勾配強度を比較します。

    c) ピクセルの勾配強度が最大の場合は保持、そうでない場合は抑制 (削除、0 に設定)
MT ( m , n ) = { M ( m , n ) 、M(m, n) > T 0 の場合、そうでなければ M_T (m,n) = \begin{cases} M(m,n), & \text {if M(m,n) > T}\\ 0, & \text {そうでなければ} \end{cases}MT(メートル,n )={ M ( m ,n ) 0 ,M(m,n) > Tの場合そうでなければ
例: [この例は、Python - Opencv の Canny エッジ検出からのものです]

    点 A は画像のエッジに垂直な位置にあります.勾配の方向はエッジに垂直です.点 B と点 C は勾配の方向に位置しています.したがって、点 A と点 B, 点 C を確認して、点 A がローカルであるかどうかを判断します点 A が極大値の場合は次の段階に進み、点 A が極大値でない場合は抑制されて 0 に設定されます。

    最終的に、境界で最も明るい細い線が保持されます


4. 二重しきい値アプローチを適用して、可能な (潜在的な) 境界を決定する

    この段階では、どのエッジが実際のエッジであり、どのエッジがそうでないかを決定します

    非最大抑制後の画像には、まだ多くのノイズ ポイントがあります。Canny アルゴリズムでは、二重しきい値処理と呼ばれる手法が適用されます。つまり、上限のしきい値 maxVal と下限のしきい値 minVal を設定します. 画像内のピクセルが上限のしきい値よりも大きい場合、それは境界 (強いエッジと呼ばれる) と見なされ、下限のしきい値よりも小さい場合は境界と見なされます。 , それは境界であってはなりません. 2つの違い 間にあるものは候補と見なされ (弱いエッジ、弱いエッジと呼ばれます)、さらに処理する必要があります.それらがエッジであると判断されたピクセルに隣接している場合, それらは判定されます.それ以外の場合は非エッジです。


5.ヒステリシス手法を使用して境界を追跡する

    この段階は、弱い境界にさらに対処することです

    一般的な考え方は、強い境界に接続された弱い境界は境界と見なされ、他の弱い境界は抑制されるというものです。
    真のエッジに起因する弱いエッジ ピクセルは強いエッジ ピクセルに接続されますが、ノイズ応答は接続されません。エッジ接続を追跡するために、弱いエッジ ピクセルとその 8 つの隣接ピクセルを見ることによって、そのうちの 1 つが強いエッジ ピクセルである限り、弱いエッジ ポイントを実際のエッジとして保持できます。


Opencv は Canny エッジ検出を実装します

OpenCV は cv2.canny 関数を提供します。

edge = cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient ]]])

パラメータ image - 入力画像, シングルチャンネルのグレースケール画像でなければなりません.
パラメータ threshold1 と threshold2 - 閾値 minVal と maxVal に対応します.
パラメータ angularSize - 画像抽出のためのソーベルカーネルのサイズを計算するために使用されます. デフォルトは 3 です.
パラメータ L2gradient -計算勾配式を指定します. パラメータが True の場合, 勾配計算式 (3) (4) が使用され, その精度が高くなります. それ以外の場合, 使用される勾配計算式は: G = ∣ G x ∣ + ∣ G y ∣ G = |G_x| + |G_y|G=∣G _×+∣G _y . このパラメータのデフォルトは False です。

例えば

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img = cv.imread('test.jpg',0)
edges = cv.Canny(img, 100, 200)

plt.subplot(121),plt.imshow(img,cmap = 'gray')
plt.title('Original Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(edges,cmap = 'gray')
plt.title('Edge Image'), plt.xticks([]), plt.yticks([])

plt.show()

結果は次のとおりです。
キャニーエッジ検出結果

手書きコード

ステートメント: コードのこの部分 [ Python が Canny 演算子のエッジ検出を実装しています]

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def smooth(image, sigma = 1.4, length = 5):
    """ Smooth the image
    Compute a gaussian filter with sigma = sigma and kernal_length = length.
    Each element in the kernal can be computed as below:
        G[i, j] = (1/(2*pi*sigma**2))*exp(-((i-k-1)**2 + (j-k-1)**2)/2*sigma**2)
    Then, use the gaussian filter to smooth the input image.

    Args:
        image: array of grey image
        sigma: the sigma of gaussian filter, default to be 1.4
        length: the kernal length, default to be 5

    Returns:
        the smoothed image
    """
    # Compute gaussian filter
    k = length // 2
    gaussian = np.zeros([length, length])
    for i in range(length):
        for j in range(length):
            gaussian[i, j] = np.exp(-((i-k) ** 2 + (j-k) ** 2) / (2 * sigma ** 2))
    gaussian /= 2 * np.pi * sigma ** 2
    # Batch Normalization
    gaussian = gaussian / np.sum(gaussian)

    # Use Gaussian Filter
    W, H = image.shape
    new_image = np.zeros([W - k * 2, H - k * 2])

    for i in range(W - 2 * k):
        for j in range(H - 2 * k):
            # 卷积运算
            new_image[i, j] = np.sum(image[i:i+length, j:j+length] * gaussian)

    new_image = np.uint8(new_image)
    return new_image

def get_gradient_and_direction(image):
    """ Compute gradients and its direction
    Use Sobel filter to compute gradients and direction.
         -1 0 1        -1 -2 -1
    Gx = -2 0 2   Gy =  0  0  0
         -1 0 1         1  2  1

    Args:
        image: array of grey image

    Returns:
        gradients: the gradients of each pixel
        direction: the direction of the gradients of each pixel
    """
    Gx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
    Gy = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])

    W, H = image.shape
    gradients = np.zeros([W - 2, H - 2])
    direction = np.zeros([W - 2, H - 2])

    for i in range(W - 2):
        for j in range(H - 2):
            dx = np.sum(image[i:i+3, j:j+3] * Gx)
            dy = np.sum(image[i:i+3, j:j+3] * Gy)
            gradients[i, j] = np.sqrt(dx ** 2 + dy ** 2)
            if dx == 0:
                direction[i, j] = np.pi / 2
            else:
                direction[i, j] = np.arctan(dy / dx)

    gradients = np.uint8(gradients)
    return gradients, direction

def NMS(gradients, direction):
    """ Non-maxima suppression
    Args:
        gradients: the gradients of each pixel
        direction: the direction of the gradients of each pixel

    Returns:
        the output image
    """
    W, H = gradients.shape
    nms = np.copy(gradients[1:-1, 1:-1])

    for i in range(1, W - 1):
        for j in range(1, H - 1):
            theta = direction[i, j]
            weight = np.tan(theta)
            if theta > np.pi / 4:
                d1 = [0, 1]
                d2 = [1, 1]
                weight = 1 / weight
            elif theta >= 0:
                d1 = [1, 0]
                d2 = [1, 1]
            elif theta >= - np.pi / 4:
                d1 = [1, 0]
                d2 = [1, -1]
                weight *= -1
            else:
                d1 = [0, -1]
                d2 = [1, -1]
                weight = -1 / weight

            g1 = gradients[i + d1[0], j + d1[1]]
            g2 = gradients[i + d2[0], j + d2[1]]
            g3 = gradients[i - d1[0], j - d1[1]]
            g4 = gradients[i - d2[0], j - d2[1]]

            grade_count1 = g1 * weight + g2 * (1 - weight)
            grade_count2 = g3 * weight + g4 * (1 - weight)

            if grade_count1 > gradients[i, j] or grade_count2 > gradients[i, j]:
                nms[i - 1, j - 1] = 0
    return nms

def double_threshold(nms, threshold1, threshold2):
    """ Double Threshold
    Use two thresholds to compute the edge.

    Args:
        nms: the input image
        threshold1: the low threshold
        threshold2: the high threshold

    Returns:
        The binary image.
    """
    visited = np.zeros_like(nms)
    output_image = nms.copy()
    W, H = output_image.shape

    def dfs(i, j):
        if i >= W or i < 0 or j >= H or j < 0 or visited[i, j] == 1:
            return
        visited[i, j] = 1
        if output_image[i, j] > threshold1:
            output_image[i, j] = 255
            dfs(i-1, j-1)
            dfs(i-1, j)
            dfs(i-1, j+1)
            dfs(i, j-1)
            dfs(i, j+1)
            dfs(i+1, j-1)
            dfs(i+1, j)
            dfs(i+1, j+1)
        else:
            output_image[i, j] = 0

    for w in range(W):
        for h in range(H):
            if visited[w, h] == 1:
                continue
            if output_image[w, h] >= threshold2:
                dfs(w, h)
            elif output_image[w, h] <= threshold1:
                output_image[w, h] = 0
                visited[w, h] = 1

    for w in range(W):
        for h in range(H):
            if visited[w, h] == 0:
                output_image[w, h] = 0
    return output_image
           
if __name__ == "__main__":
    # code to read image
    image = cv.imread('test.jpg',0)
    cv.imshow("Original",image)
    smoothed_image = smooth(image)
    cv.imshow("GaussinSmooth(5*5)",smoothed_image)
    gradients, direction = get_gradient_and_direction(smoothed_image)
    # print(gradients)
    # print(direction)
    nms = NMS(gradients, direction)
    output_image = double_threshold(nms, 40, 100)
    cv.imshow("outputImage",output_image)
    cv.waitKey(0)

結果は次のとおりです。
手書きコードの結果

参考記事

おすすめ

転載: blog.csdn.net/qq_38828370/article/details/119696756