記事ディレクトリ
要約 : Canny エッジ検出アルゴリズムは、1986 年にコンピューター科学者の John F. Canny によって提案されました。アルゴリズムを提供するだけでなく、一連のエッジ検出理論も提供し、段階的にエッジ検出を実装する方法を説明します。Canny 検出アルゴリズムは次の段階で構成されます。
- 画像のグレースケール
- ガウスぼかし
- 画像勾配、勾配の大きさ、勾配方向の計算
- NMS (非最大抑制)
- 二重閾値による境界選択
1. キャニーエッジ検出のために opencv を呼び出します。
状況を把握するために Canny を適用したいだけの場合は、Canny の特定の原則と実装を読む必要はありません。Python の opencv ライブラリがこの機能を実現するための優れた機能を提供しているためです。例を使用して、鋭いエッジ検出に opencv を使用する方法を説明します。
これは元の画像と、opencv を使用したキャニー エッジ検出の結果画像です。
コードの実装は次のとおりです。
import cv2 #导入opencv库
#读取图片
img = cv2.imread("images/2007\_000032.jpg")
#进行canny边缘检测
edge = cv2.Canny(img,50,150)
#保存结果
cv2.imwrite('test.jpg',edge)
これら 4 行のコードの鍵となるのはcv2.Canny関数です。そのパラメータについて詳しく説明します。お役に立てば幸いです。OpenCV-Python の Canny 関数のプロトタイプは次のとおりです。
cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])
必須パラメータ:
- 最初のパラメータは処理される元の画像であり、単一チャネルのグレースケール画像である必要があります。
- 2 番目のパラメータはしきい値 1 です。
- 3 番目のパラメータは Threshold2 です。
このうち、大きい閾値 2 は画像内の明らかなエッジを検出するために使用されますが、一般に検出効果はそれほど完璧ではなく、エッジ検出は断続的です。したがって、この時点では、より小さい第 1 閾値がこれらの不連続エッジを接続するために使用されます。
オプションのパラメーターの apertureSize は、Sobel オペレーターのサイズです。L2gradient パラメータはブール値です。true の場合、より正確な L2 ノルムが計算に使用されます (つまり、2 方向の逆数の二乗の合計がオープンされます)。そうでない場合は、L1 ノルムが (直接使用されます) 2 方向微分値の絶対値を加算)。
この時点で、プロジェクトに実際に鋭いエッジ検出を適用できます。もちろん、鋭いエッジ検出の原理を理解したい場合は、読み続けてください。
2. 画像のグレースケール
画像の場合、その境界だけを気にする場合、境界を検出するための情報を提供するには単一チャネルの画像で十分です。したがって、R、G、B の 3 チャンネル画像や、さらに高次元のハイパースペクトル リモート センシング画像をグレースケール化することができます。グレースケールは実際には次元削減操作であり、冗長データを削減し、計算オーバーヘッドを削減します。グレースケール RGB 画像の方法は次のとおりです。
# 灰度化
def gray(self, img\_path):
"""
计算公式:
Gray(i,j) = [R(i,j) + G(i,j) + B(i,j)] / 3
or :
Gray(i,j) = 0.299 \* R(i,j) + 0.587 \* G(i,j) + 0.114 \* B(i,j)
"""
# 读取图片
img = plt.imread(img_path)
# BGR 转换成 RGB 格式
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 灰度化
img_gray = np.dot(img_rgb[...,:3], [0.299, 0.587, 0.114])
return img_gray
3. ガウスブラー処理
ガウスぼかしは、実際にはグレースケール画像のノイズを除去します。数学的な観点から見ると、画像のガウスぼかしプロセスは、画像と正規分布の畳み込みです。ガウス フィルタリングを実行する前に、まずガウス フィルタ (カーネル) を取得する必要があります。ガウスフィルターを取得するにはどうすればよいですか? 実際、ガウス関数は離散化され、フィルター内の対応する水平および垂直座標インデックスがガウス関数に代入されて、対応する値が取得されます。フィルタのサイズが異なると値も異なります。2 次元ガウス関数と (2k+1)x(2k+1) フィルタの計算式は次のとおりです。
一般的に使用されるガウス フィルターのサイズは 5x5、σ=1.4 のガウス フィルターです。以下は、5x5 ガウス フィルターの実装コードです。
# 去除噪音 - 使用 5x5 的高斯滤波器
def smooth(self, img\_gray):
# 生成高斯滤波器
"""
要生成一个 (2k+1)x(2k+1) 的高斯滤波器,滤波器的各个元素计算公式如下:
H[i, j] = (1/(2\*pi\*sigma\*\*2))\*exp(-1/2\*sigma\*\*2((i-k-1)\*\*2 + (j-k-1)\*\*2))
"""
sigma1 = sigma2 = 1.4
gau_sum = 0
gaussian = np.zeros([5, 5])
for i in range(5):
for j in range(5):
gaussian[i, j] = math.exp((-1/(2*sigma1*sigma2))*(np.square(i-3)+ np.square(j-3)))/(2*math.pi*sigma1*sigma2)
gau_sum = gau_sum + gaussian[i, j]
# 归一化处理
gaussian = gaussian / gau_sum
# 高斯滤波
W, H = img_gray.shape
new_gray = np.zeros([W-5, H-5])
for i in range(W-5):
for j in range(H-5):
new_gray[i, j] = np.sum(img_gray[i:i+5, j:j+5] * gaussian)
return new_gray
4. 画像の勾配、勾配の大きさ、勾配の方向の計算
このステップの重要性はどれだけ強調してもしすぎることはありません。直感的に言えば、画像上の境界付近の画素値が大きく変化することが分かります。オブジェクト内のほとんどのピクセル値は類似しています。このようにして、現在のピクセルとその近くのピクセルのピクセル値の差を計算して、ピクセルがオブジェクトの内部にあるか境界にあるかを判断できます。この差は画像の勾配と呼ばれます。勾配の大きさと勾配の方向は画像の勾配から計算されます。
具体的には、一次導関数を使用して勾配を計算します。
上記の式の実際の操作は、現在のピクセルの次のピクセルから現在のピクセルを減算することです。このときΔ x = 1 \Delta x=1Δx_ _=1 ;
勾配には、x方向の勾配とy方向の勾配とが含まれる。それらは 2 つのベクトルです。勾配の大きさは、次の 2 つのベクトルのベクトル和です。
勾配の大きさがベクトルになったので、その方向を計算する必要があります。
実現するには次のコードを使用します。
# 计算梯度幅值
def gradients(self, new_gray):
"""
:type: image which after smooth
:rtype:
dx: gradient in the x direction
dy: gradient in the y direction
M: gradient magnitude
theta: gradient direction
"""
W, H = new_gray.shape
dx = np.zeros([W-1, H-1])
dy = np.zeros([W-1, H-1])
M = np.zeros([W-1, H-1])
theta = np.zeros([W-1, H-1])
for i in range(W-1):
for j in range(H-1):
dx[i, j] = new\_gray[i+1, j] - new\_gray[i, j]
dy[i, j] = new\_gray[i, j+1] - new\_gray[i, j]
# 图像梯度幅值作为图像强度值
M[i, j] = np.sqrt(np.square(dx[i, j]) + np.square(dy[i, j]))
# 计算 θ - artan(dx/dy)
theta[i, j] = math.atan(dx[i, j] / (dy[i, j] + 0.000000001))
return dx, dy, M, theta
計算された勾配の大きさで、実際に画像の境界 (つまり、関数の戻り値の M) が得られます。次のように:
ただし、このエッジには 2 つの問題があることは簡単にわかります。
- 厚いエッジ。
- 途切れ途切れのエッジがたくさんあります。
これら 2 つの問題に対して、NMS と二重閾値境界の選択には次の 2 つのステップがあります。
5. NMS(非最大抑制)
理想的には、結果として得られるエッジは非常に薄いはずです。したがって、エッジを薄くするには非最大抑制を実行する必要があります。原理は単純です。勾配マトリックス上のすべての点を走査し、エッジ方向の最大値を持つピクセルを保持します。下の写真のようになります。図の黒と灰色は境界を表します。NMS を使用して極大値 (つまり、画像内の黒) を見つけ、他の位置の値 (つまり、画像内の灰色) を 0 に設定します。
NMS について詳しく説明します。NMSは、上、下、左、右、左上、左下、右上、右下の8つのフィールドで実行されます(もちろん、比較するときにこの点を他の8つの点と比較する必要はありません。比較するだけです)これは理解しやすいです。現在の値が属するエッジの極大値である必要があるだけで、他のエッジの極大値である必要はないからです。) 以下の図に示すように、 8 つのポイントは近くの 8 つのフィールドです。
NMS は極大値を見つけるものであるため、現在のピクセルの勾配を他の方向と比較する必要があります。下図に示すように、g1、g2、g3、g4 は C の 8 つの領域内の 4 点であり、青線は C の勾配方向です。C が極大値の場合、点 C の勾配振幅は、勾配方向線と g1g2、g4g3 の 2 つの交点の勾配振幅よりも大きくなります。つまり、点 dTemp1 および dTemp2 の勾配振幅よりも大きくなります。前述したように、dTemp1 と dTemp2 は整数ピクセルではなくサブピクセルであるため、この方法では最良の効果を得ることができません。サブピクセルとは、2 つの物理ピクセルの間にピクセルがあることを意味します。では、サブピクセルの勾配の大きさを見つけるにはどうすればよいでしょうか? 線形補間法を使用して、g1 と g2 の間の dTemp1 の重みを計算し、その勾配の大きさを取得できます。次のように計算されます。
weight = |gx| / |gy| or |gy| / |gx|
dTemp1 = weight*g1 + (1-weight)*g2
dTemp2 = weight*g3 + (1-weight)*g4
計算には 2 つの状況があります (現在のピクセルが dtemp1 および dtemp2 のサイズと比較され、これら 2 つの値より大きい場合は保持され、いずれか 1 つより小さい場合、その値は 0 になります) ):
-
次の 2 つの図は、y 方向の勾配値が比較的大きい場合、つまり勾配方向が y 軸に近い場合です。したがって、このとき g2 と g4 は C の上下の位置にあり、重み = |gy| / |gx| となります。左図はx方向とy方向の勾配の符号が同じ場合、右図はx方向とy方向の勾配の符号が逆の場合である。
-
次の 2 つの図は、x 方向の勾配値が比較的大きい場合、つまり勾配方向が x 軸に近い場合です。したがって、このとき g2 と g4 は C の左右の位置にあり、重み = |gy| / |gx| となります。左図はx方向とy方向の勾配の符号が同じ場合、右図はx方向とy方向の勾配の符号が逆の場合である。
コードは次のように実装されます。
def NMS(self, M, dx, dy):
d = np.copy(M)
W, H = M.shape
NMS = np.copy(d)
NMS[0, :] = NMS[W-1, :] = NMS[:, 0] = NMS[:, H-1] = 0
for i in range(1, W-1):
for j in range(1, H-1):
# 如果当前梯度为0,该点就不是边缘点
if M[i, j] == 0:
NMS[i, j] = 0
else:
gradX = dx[i, j] # 当前点 x 方向导数
gradY = dy[i, j] # 当前点 y 方向导数
gradTemp = d[i, j] # 当前梯度点
# 如果 y 方向梯度值比较大,说明导数方向趋向于 y 分量
if np.abs(gradY) > np.abs(gradX):
weight = np.abs(gradX) / np.abs(gradY) # 权重
grad2 = d[i-1, j]
grad4 = d[i+1, j]
# 如果 x, y 方向导数符号一致
# 像素点位置关系
# g1 g2
# c
# g4 g3
if gradX * gradY > 0:
grad1 = d[i-1, j-1]
grad3 = d[i+1, j+1]
# 如果 x,y 方向导数符号相反
# 像素点位置关系
# g2 g1
# c
# g3 g4
else:
grad1 = d[i-1, j+1]
grad3 = d[i+1, j-1]
# 如果 x 方向梯度值比较大
else:
weight = np.abs(gradY) / np.abs(gradX)
grad2 = d[i, j-1]
grad4 = d[i, j+1]
# 如果 x, y 方向导数符号一致
# 像素点位置关系
# g3
# g2 c g4
# g1
if gradX * gradY > 0:
grad1 = d[i+1, j-1]
grad3 = d[i-1, j+1]
# 如果 x,y 方向导数符号相反
# 像素点位置关系
# g1
# g2 c g4
# g3
else:
grad1 = d[i-1, j-1]
grad3 = d[i+1, j+1]
# 利用 grad1-grad4 对梯度进行插值
gradTemp1 = weight \* grad1 + (1 - weight) \* grad2
gradTemp2 = weight \* grad3 + (1 - weight) \* grad4
# 当前像素的梯度是局部的最大值,可能是边缘点
if gradTemp >= gradTemp1 and gradTemp >= gradTemp2:
NMS[i, j] = gradTemp
else:
# 不可能是边缘点
NMS[i, j] = 0
return NMS
6. 二重閾値の境界選択
この段階では、どのエッジが実際のエッジであり、どのエッジがそうでないかを決定します。このためには、minVal と maxVal という 2 つのしきい値を設定する必要があります。maxVal を超える勾配を持つエッジは間違いなく真のエッジですが、minVal を下回るエッジは間違いなく非エッジであるため、破棄されます。これら 2 つのしきい値の間にあるエッジは、接続性に基づいてエッジまたは非エッジとして分類され、「信頼できるエッジ」ピクセルに接続されている場合はエッジの一部とみなされます。それ以外の場合は、これも破棄されます。コードは次のようになります。
def double\_threshold(self, NMS):
W, H = NMS.shape
DT = np.zeros([W, H])
# 定义高低阈值
TL = 0.1 \* np.max(NMS)
TH = 0.3 \* np.max(NMS)
for i in range(1, W-1):
for j in range(1, H-1):
# 双阈值选取
if (NMS[i, j] < TL):
DT[i, j] = 0
elif (NMS[i, j] > TH):
DT[i, j] = 1
# 连接
elif (NMS[i-1, j-1:j+1] < TH).any() or (NMS[i+1, j-1:j+1].any() or (NMS[i, [j-1, j+1]] < TH).any()):
DT[i, j] = 1
return DT
すべての手順を完了すると、結果が次の図に表示されます。