【エッジ検出】OpenCV/Cannyの性能はいかに

トライアル

この記事は2020年に別サイトへ投稿した記事を一部修正して移行したものです。

概要

OpenCVを使って画像のエッジ検出をしてみましょう。
エッジ検出とは、画素値を縦横にたどって値の変化が大きい部分を検出する処理です。
数学的に言うと、画像の中の連続する画素値の微分係数、すなわち輝度勾配が大きい部分を検出する処理ということになります。

学習すること

  • OpenCVを使った画像分析プログラミング
  • エッジ検出アルゴリズム

前提知識

  • Python3プログラミング

開発環境

  • Python3実行環境(Windows、MacOS、Linux上のPython仮想環境)
    ※当記事ではWindows10を使用します。
  • OpenCVライブラリ
    ※当記事では opencv-python: 4.5.5.62 を使用します。

開発実施

OpenCVで分析対象の画像を表示してみよう

エッジ検出する前に、まずは分析対象となる画像を表示してみましょう。
今回は縦横のエッジがわかりやすい画像を用意してみました。
下記のとおり、OpenCVによる画像の表示は実に簡単です。

import cv2

TARGET_IMAGE = 'shogi.jpg'

# 画像ファイルをグレースケールで読み込む。
img = cv2.imread(TARGET_IMAGE, cv2.IMREAD_GRAYSCALE)

# 第1引数はウィンドウ名
cv2.imshow('original', img)

# ウィンドウを表示したまま、
# 何らかのキーが押されるまで待機する。
cv2.waitKey(0)

# ウィンドウを閉じる。
cv2.destroyAllWindows()

エッジ検出のために色情報は不要なので、最初にグレースケールへ変換してしまいます。
グレースケールとは、RGBの3つの値を按分して、色強度という1つの値にまとめることです。

色強度 = 0.2989R + 0.587G + 0.114B

OpenCVが提供するエッジ検出アルゴリズムについて調べてみよう

OpenCVでエッジ検出するにはいくつかの方法があるようです。
代表的なアルゴリズムは次の3つです。

  • Sobelフィルタ
  • Laplacianフィルタ
  • Canny

Sobelフィルタは、縦方向のエッジ検出、横方向のエッジ検出をそれぞれ個別に行う方法です。
Laplacianフィルタは、縦横指定しなくても両方検出してくれる方法です。
Cannyは、4つのステップによって精度の高い検出を実現する方法です。
今回は、一般的に最も使用されており精度の高そうなCannyを使うことにします。

プログラミングの前に、Cannyアルゴリズムの「4つのステップ」を具体的に確認しておきましょう。

  1. ノイズ削減
  2. 輝度勾配検出
  3. 非極大値の抑制
  4. 閾値処理

ノイズ削減
まず最初に、Gaussianフィルタを使って細かいノイズを除去します。
GaussianフィルタはOpenCVで最も良く使われるノイズ除去アルゴリズムです。

輝度勾配検出
連続する画素値を微分して、勾配が大きい箇所を検出します。
Sobelフィルタを使って縦横両方実施します。

非極大値の抑制
検出した勾配のうち、エッジは残しつつグラデーションだけを除去します。
グラデーションはエッジとはみなしません。
エッジとグラデーションの違いを判別するために、勾配上の近傍点(画素値の変化が大きい箇所の近くの点)を利用します。
輝度勾配検出した点と同じ勾配上(Sobelフィルタで検出したエッジ線上)の両脇の近傍点を比較して、エッジ検出点が「極値」であるか「変曲点」であるかを判定します。
「極値」すなわち極大値か極小値ならばエッジと判定します。
「変曲点」ならばグラデーションと判定してエッジから除去します。

閾値処理
画素値の変化が「大きい」箇所をエッジとみなすためには、どれくらいの変化を「大きい」とみなすべきかの閾値が必要です。
「閾値処理」ステップではその閾値を調節します。
閾値以上の勾配を持つ点をエッジとみなし、そうでない点は単なる模様とみなします。
閾値にはMAXとMINの2種類あります。
ある点の勾配がMAX以上であればそこはエッジになります。それとは別の点がMAX閾値を下回っていても、さきほどのエッジ点とつながった線上にあればエッジとみなします。ただし線上であってもMIN閾値を下回ればエッジから除外します。
一方、ある点の勾配がMIN以下であればそこはエッジから除外します。それとは別の点がMIN閾値を上回っても、さきほどの除外点とつながった線上にあればエッジとはみなしません。ただし線上であってもMAX閾値を上回ればエッジとみなします。

ヒステリシスになってるんだね!

濃いエッジ線がずっと伸びて次第に薄くなっていてもエッジとみなし、
薄い模様線がずっと伸びて次第に濃くなっていても模様とみなすというわけです。

2つの閾値は、ユーザがニーズに合わせて調整しながら指定します。

Cannyでエッジ検出しよう!

ではCannyを使ってエッジ検出してみましょう。
閾値はひとまず、MIN = 100、MAX = 180 としてみました。

import cv2

TARGET_IMAGE = 'shogi.jpg'

img = cv2.imread(TARGET_IMAGE, cv2.IMREAD_GRAYSCALE)
img_canny = cv2.Canny(img, 100, 180)

cv2.imshow('canny', img_canny)
cv2.waitKey(0)
cv2.destroyAllWindows()

なんて簡単なんでしょう。
表示結果は次のとおりです。

Cannyによるエッジ検出結果


いいかんじですね。
閾値調節の必要はなさそうですが、せっかくなので閾値のMINとMAXをいじってみましょう。

Cannyの閾値を調節してみよう

ウィンドウにトラックバーを2つ設置して、MAXとMINの閾値を調節できるようにしてみました。
トラックバー1とトラックバー2の2つの入力値のうち、大きい方をMAX閾値、小さい方をMIN閾値としてCannyに指定するようにしています。

import cv2

TARGET_IMAGE = 'shogi.jpg'
NAME_WINDOW = 'Canny'
NAME_THRSH_1 = 'Threshold_1'
NAME_THRSH_2 = 'Threshold_2'
THRSH_MAX = 512
KEY_ESC = 27

thrsh_1, thrsh_2 = (0, 0)


def nothing(x):
    pass


img = cv2.imread(TARGET_IMAGE, cv2.IMREAD_GRAYSCALE)

cv2.namedWindow(NAME_WINDOW)
cv2.createTrackbar(NAME_THRSH_2, NAME_WINDOW, thrsh_2, THRSH_MAX, nothing)
cv2.createTrackbar(NAME_THRSH_1, NAME_WINDOW, thrsh_1, THRSH_MAX, nothing)

while True:

    thrsh_1 = cv2.getTrackbarPos(NAME_THRSH_1, NAME_WINDOW)
    thrsh_2 = cv2.getTrackbarPos(NAME_THRSH_2, NAME_WINDOW)

    thrsh_min, thrsh_max = min(thrsh_1, thrsh_2), max(thrsh_1, thrsh_2)

    img_canny = cv2.Canny(img, thrsh_min, thrsh_max)
    cv2.imshow(NAME_WINDOW, img_canny)
    if cv2.waitKey(10) == KEY_ESC:
        break

cv2.destroyAllWindows()

トラックバー作成時、cv2.createTrackbar()にコールバック関数を設定する方法もあるよ。

でもここでは、OpenCV Pythonチュートリアルにしたがってコールバックは使わずダミー関数にしているよ。

このプログラムを実行して閾値を手動で調節した結果が、下記の動画です。
将棋盤の「マス目」と「コマの輪郭」をエッジとして検出しようとしています。
初期状態では閾値が0のため、盤上の木目模様をすべてエッジとして認識してしまっています。
まず上のトラックバーを大きくして、マス目とコマの輪郭がくっきり検出できる上限を探しています。
続いて下のトラックバーを少しずつ大きくして、わずかに残っている木目の模様をエッジから除去していきます。

ユーザによるCanny閾値の調節

今後の課題

  • 複雑な曲線を含む画像でCannyエッジ検出をしてみる。
  • 動画データに対してCannyエッジ検出をしてみる。
  • さまざまな画像に対して最適な閾値を自動的に発見する方法を調査する。

まとめ

OpenCVを使って画像のエッジ検出をしてみました。
OpenCVが提供するエッジ検出にはいくつかの方法がありますが、そのうち最も実用的なCannyというアルゴリズムを使ってみました。
Cannyはエッジ検出前にGaussianフィルタでノイズ除去してくれたり、縦横両方のSobelフィルタでエッジ検出してくれたり、さらには閾値を使って精度を調節する手段をユーザに提供してくれたりと、非常に性能の高いアルゴリズムであることがわかりました。

実行環境

Python: 3.8
opencv-python: 4.5.5.62

コメント

タイトルとURLをコピーしました