ITや趣味など気軽に投稿しています。

【Python】OpenCVで画像のエッジを検出する

画像処理におけるエッジ検出とは、画像中の物体の輪郭(境界)を抽出する技術です。物体のエッジを検出することで、画像中のオブジェクトをより鮮明に把握できます。

この記事では、1次微分フィルタ、Prewitt、Sobel、Canny、Laplacianの5種類のエッジ検出手法を解説します。

エッジ検出の原理

エッジの検出は、明るさ(輝度)が大きく変化する部分を抽出することで実現します。例えば、白い背景に黒い物体がある場合、白から黒に変わる境界が物体の輪郭です。

具体的には、各ピクセルの輝度を微分し、微分値が大きくなる部分をエッジとして検出します。画像のピクセルは離散値なので、隣り合う画素間の差を取ることで近似的な微分として扱います。

1次微分フィルタ(cv2.filter2D())

最もシンプルなエッジ検出手法です。以下のカーネルを使用し、注目画素と隣接ピクセルとの差でエッジを検出します。

1次微分フィルタのカーネル

Kxでは水平方向、Kyでは垂直方向の微分値が得られます。

Pythonで実装

OpenCVには1次微分フィルタ専用の関数がないため、カーネルを定義してcv2.filter2D()でフィルタリングします。

import cv2
import matplotlib.pyplot as plt
import numpy as np
import japanize_matplotlib

# 画像の読み込み
image = cv2.imread("input/flowers.png", cv2.IMREAD_GRAYSCALE)

# 水平方向微分のカーネル
kernel_x = np.array([[0, -1, 0], [0, 0, 0], [0, 1, 0]])
# 垂直方向微分のカーネル
kernel_y = np.array([[0, 0, 0], [-1, 0, 1], [0, 0, 0]])

# フィルタ2Dを適用
first_derivative_x = cv2.filter2D(image, cv2.CV_64F, kernel_x)
first_derivative_y = cv2.filter2D(image, cv2.CV_64F, kernel_y)

# エッジの強度を計算
first_derivative = np.sqrt(first_derivative_x**2 + first_derivative_y**2)

# 画像を表示
plt.figure(figsize=(10, 6))
plt.subplot(1, 2, 1)
plt.imshow(image, cmap='gray')
plt.title('Original')
plt.axis("off")
plt.subplot(1, 2, 2)
plt.imshow(first_derivative, cmap='gray')
plt.title('一次微分フィルタ')
plt.axis("off")
plt.show()
1次微分フィルタ

Prewittフィルタ(cv2.filter2D())

1次微分フィルタはノイズにも反応しやすいという問題があります。Prewittフィルタは、対象を斜めを含む周辺ピクセルに拡大することで平滑化処理を加え、ノイズの影響を低減します。

Prewittフィルタのカーネル

Pythonで実装

import cv2
import matplotlib.pyplot as plt
import numpy as np
import japanize_matplotlib

image = cv2.imread("input/flowers.png", cv2.IMREAD_GRAYSCALE)

kernel_x = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]])
kernel_y = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]])

prewitt_x = cv2.filter2D(image, -1, kernel_x)
prewitt_y = cv2.filter2D(image, -1, kernel_y)
prewitt = np.sqrt(prewitt_x**2 + prewitt_y**2)

plt.figure(figsize=(10, 6))
plt.subplot(2, 2, 1)
plt.imshow(image, cmap='gray')
plt.title('Original')
plt.axis("off")
plt.subplot(2, 2, 2)
plt.imshow(prewitt, cmap='gray')
plt.title('Prewitt Filter')
plt.axis("off")
plt.subplot(2, 2, 3)
plt.imshow(prewitt_x, cmap='gray')
plt.title('水平方向')
plt.axis("off")
plt.subplot(2, 2, 4)
plt.imshow(prewitt_y, cmap='gray')
plt.title('垂直方向')
plt.axis("off")
plt.show()
Prewittフィルタ

Sobelフィルタ(cv2.Sobel())

Sobelフィルタも1次微分フィルタに分類されますが、中心に隣接するピクセルが強調されるカーネルを使用します。

Sobelフィルタのカーネル

メリット: コントラストの小さいエッジもしっかりと強調できる デメリット: ノイズにも反応しやすい

Pythonで実装

OpenCVにはcv2.Sobel()関数が用意されています。

cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
引数説明
ddepth出力画像の深度。-1で入力画像と同じ型
dx1でX軸方向の微分を実施。0で微分しない
dy1でY軸方向の微分を実施。0で微分しない
ksizeカーネルのサイズ
scaleカーネル要素にかかる倍率
delta計算結果に加算される定数値
import cv2
import matplotlib.pyplot as plt

image = cv2.imread("input/flowers.png", cv2.IMREAD_GRAYSCALE)

sobelx = cv2.Sobel(src=image, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=1)
sobely = cv2.Sobel(src=image, ddepth=cv2.CV_32F, dx=0, dy=1, ksize=1)
sobelxy = cv2.Sobel(src=image, ddepth=cv2.CV_32F, dx=1, dy=1, ksize=1)

plt.figure(figsize=(10, 6))
plt.subplot(2, 2, 1)
plt.imshow(cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
plt.title('Original')
plt.axis("off")
plt.subplot(2, 2, 2)
plt.imshow(cv2.cvtColor(sobelx, cv2.COLOR_RGB2BGR))
plt.title('Sobel X')
plt.axis("off")
plt.subplot(2, 2, 3)
plt.imshow(cv2.cvtColor(sobely, cv2.COLOR_RGB2BGR))
plt.title('Sobel Y')
plt.axis("off")
plt.subplot(2, 2, 4)
plt.imshow(cv2.cvtColor(sobelxy, cv2.COLOR_RGB2BGR))
plt.title('Sobel XY')
plt.axis("off")
plt.show()
Sobelフィルタ

Cannyフィルタ(cv2.Canny())

Cannyフィルタは、ガウシアンぼかしとSobelフィルタを組み合わせた高精度なエッジ検出手法です。ノイズを除去しつつ、コントラストの弱いエッジも的確に検出できます。

Cannyフィルタの処理は以下の4ステップで構成されます。

  1. ガウシアンぼかし: ノイズを除去する
  2. Sobelフィルタ: エッジの勾配を計算する
  3. 非極大値抑制処理: エッジ以外のピクセルを除去し、1ピクセル幅のシャープなエッジを抽出する
  4. ヒステリシス閾値処理: 断片的なエッジを連結し、1本のエッジとして形成する

処理の詳細

非極大値抑制処理では、注目画素の画素値と勾配方向に隣り合う2つの画素の値を比較します。注目画素が最大でなければ、その値を0に置き換えます。

ヒステリシス閾値処理では、2つの閾値(最大閾値・最小閾値)を使用します。

  • 最大閾値より高い → 信頼性の高い輪郭として残す
  • 最小閾値より低い → 信頼性の低い輪郭として消去する
  • 2つの閾値の間 → 隣接ピクセルが高信頼なら残す、そうでなければ消去する

Pythonで実装

cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])
引数説明
threshold1ヒステリシス閾値の最小閾値
threshold2ヒステリシス閾値の最大閾値
apertureSizeSobel演算子に使用するカーネルサイズ
L2gradientTrueでユークリッド距離、False(デフォルト)でL1ノルムを使用
import cv2
import matplotlib.pyplot as plt

image = cv2.imread("input/flowers.png")
img_canny = cv2.Canny(image=image, threshold1=100, threshold2=200)

plt.figure(figsize=(10, 6))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
plt.title('Original')
plt.axis("off")
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img_canny, cv2.COLOR_RGB2BGR))
plt.title('Canny')
plt.axis("off")
plt.show()
Cannyフィルタ

エッジが1本の明確な線として抽出されていることが確認できます。

Laplacianフィルタ(cv2.Laplacian())

Laplacianフィルタ二次微分を利用したエッジ検出手法です。これまでのフィルタが一次微分だったのに対し、異なるアプローチでエッジを検出します。

4近傍と8近傍のカーネルがあります。

4近傍カーネル
8近傍カーネル

メリット: 直線的なエッジだけでなく、複雑な曲線も強調できる デメリット: わずかなノイズにも反応しやすく、不要なエッジが強調される場合がある。特定方向のみのエッジ検出には不向き

Pythonで実装

cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
引数説明
ddepth出力画像の深度。-1で入力画像と同じ型
ksizeカーネルのサイズ
scaleカーネル要素にかかる倍率
delta計算結果に追加するオフセット値
import cv2
import matplotlib.pyplot as plt

image = cv2.imread("input/flowers.png")
img_lap = cv2.Laplacian(image, cv2.CV_32F, ksize=1)

plt.figure(figsize=(10, 6))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
plt.title('Original')
plt.axis("off")
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img_lap, cv2.COLOR_RGB2BGR))
plt.title('Laplacian')
plt.axis("off")
plt.show()
Laplacianフィルタ

まとめ

今回は、5種類のエッジ検出フィルタを紹介しました。

フィルタ微分の種類OpenCV関数特徴
1次微分一次微分cv2.filter2D()シンプルだがノイズに弱い
Prewitt一次微分cv2.filter2D()平滑化でノイズ耐性向上
Sobel一次微分cv2.Sobel()コントラストの弱いエッジにも有効
Canny一次微分cv2.Canny()高精度な1ピクセル幅のエッジ検出
Laplacian二次微分cv2.Laplacian()複雑な曲線のエッジにも対応

処理の複雑さと精度はトレードオフの関係にあるため、画像の特性や用途に応じて適切なフィルタを選択してください。