2016. 8.22.
石立 喬

OpenCV3.1.0とVisual C++ 2015による画像処理と認識(7)

----- 各種エッジ検出フィルタと関連類似関数を試みる -----

 エッジ検出フィルタは、物体の輪郭を取り出し、画像認識の前処理に重要な役割を果たす。「OpenCVとVisual C++による画像処理と認識(7)」では、道路標識を対象に、グレイスケール画像のエッジ検出とRGB成分に分解しての検出を比較した。RGB成分に分けた方が良いことは分かったので、ここでは、それは繰り返さず、単純な方法で、一般風景画像を対象にし、エッジ検出を応用した類似の関数も追加して比較した。
 エッジ検出は、画像の濃度変化部分を選び出し、非線形処理をして1、0で出力するものである。しかし、濃度変化をやや強調するのみで、線形的に結果を出力する鮮鋭化も、その一種であると考え、blur関数から実現したunsharp処理を行い、photoモジュールの類似関数も試みた。道路標識についての結果も、参考までに一部紹介した。

エッジ検出フィルタのいろいろ
 OpenCVのimgprocモジュールのImage Filteringサブモジュールに用意されているエッジ検出関数には、Laplacian、Scharr、Sobel、spatialGradient(OpenCV3.0から追加)がある。また、Feature DetectionサブモジュールにはCannyがある。CannyはSobelの結果にCannyアルゴリズムを適用したものである。そのほか、photoモジュールのNon-Photorealistic Rendering(非写実的画像生成)サブモジュール(OpenCV3.0から追加)に属するdetailEnhance関数、pencilSketch関数、stlization関数もエッジ検出の応用と言えなくもない。

いろいろなエッジ検出フィルタを比較するプログラム
 図1のプログラムは下記から成っている。
1)原画像を、画像image_srcとして読み込んで表示する。
2)多くの関数は、入力画像としてグレイスケール画像が必要なので、あらかじめグレイスケール化原画像image_grayを用意する。
3)関数によっては、ノイズに敏感なので、ぼかしたグレイスケール化画像image_gray_gaussも用意する。
4)各関数を用いて、エッジ検出を行う。
    原画像、グレイスケール化原画像、ぼかしグレイスケール原画像などを選択して使用する。
    X方向とY方向が個別に出力される関数の場合は、各出力の絶対値を取り、平均を取る。
    Cannyの場合は、出力画像の反転を行う。
    Laplacianは、正負ペアの出力が得られ、絶対値を採って表示すると二本の線になるので、正出力のみを使用する。
    必要に応じてthreshold関数を使用する。

使用したOpenCV関数の説明
 各関数のデフォルト値( = で表す)は、特に言及しない限り、そのままの使用で良い結果が得られた。
◎Sobel関数
 原画像を一次微分してエッジを検出する関数で、平滑化する効果も持たせてある。dx、dyを同時に1にしてはいけない。
  Sobel(
    src ------ InputArray、入力画像
    dst ------ OutputArray、入力画像と同じサイズとチャンネル数の出力配列
    ddepth --- int、出力配列の深度、CV_32Fを推奨(threshold関数を使うため)
    dx ------- int、x方向の次数、1または0
    dy ------- int、y方向の次数、0または1
    ksize = 3 --- int、Sobel形式のカーネルのサイズ(奇数)、-1(またはCV_SCHARR)にすると3x3のScharrフィルタになる
    scale = 1 --- double、倍率
    delta = 0 ---- double、オフセット
    borderType = BORDER_DEFAULT ----- int
  )
 なお、ここでは、X方向とY方向の各絶対値を加算するだけであると、そのままthresholdをかけるには出力が大きすぎて2で割った。平均を採ることに特別の意味はない。
◎threshold関数
 この関数は、imgprocモジュールのMiscellaneous Image Transformationsに属する。入力する配列の深度(ビット数)条件があるので、16ビットの場合は、あらかじめ32ビットに変換しておく必要がある。
  double threshold(
    src ------ InputArray、8ビットまたは32ビット、1チャンネル
    dst ------ OutputArray、同上
    thresh --- double、外部から与えるしきい値
    maxval --- double、最大値、typeにTHRESH_BINARYまたはTHRESH_BINARY_INVを用いるときは255
    type ----- int
  )
 typeには、
   THRESH_BINARY ( = 0) --------- 黒地に白でエッジを表す
   THRESH_BINARY_INV ( = 1) ----- 白地に黒でエッジを表す
その他がある。
◎spatialGradient関数
 Sobel関数同様に、Image Filteringサブモジュールに属する。一つの関数で、x方向、y方向のエッジ出力が同時に得られるのは便利であるが、出力形式がCV_16SC1に固定のため(オーバーフローさせないためとの説明がある)、threshold関数に直接与えられないのが欠点である。
  spatialGradient(
    src ----- InputArray、入力画像
    dx ------ OutputArray、CV_16SC1の出力配列
    dy ------ OutputArray、同上
    ksize = 3 --- int、Sobelカーネルのサイズ(3に限る)
    borderType = BOARDER_DEFAULT
  )
◎Scharr関数
 Sobel関数同様に、Image Filteringサブモジュールに属する。Sobelの係数を大きくしたもので、より精度が良いといわれるが、実感ではほとんど変わらない。出力が大きいので、threshold関数での調整が必要になる。
  Scharr(
    src ----- InputArray、入力画像
    dst ----- OutputArray、入力画像と同じサイズとチャンネル数の出力配列
    ddepth -- int、出力配列の深度、CV_32Fを推奨(threshold関数を使うため)
    dx ------ int、x方向の次数、1または0
    dy ------ int、y方向の次数、0または1
    scale = 1 ---- double、倍率
    delta = 0 ---- double、オフセット
    borderType = BORDER_DEFAULT
  )
◎Canny関数
 imgprocモジュールのFeature Detectionサブモジュールに属する。しきい値を上下2種類与え、上のしきい値以上は、無条件にエッジと判定、下のしきい値以下は、無条件に非エッジとするが、その中間は、すでにエッジと判定されている点との接続性で判定する。事前に平滑化しておくことが推奨される。使用しているのは、縦横二つのSobelカーネルである。
 エッジ部分の最大値近辺の出力を抑制して細線化を行っているので、判定されたエッジの幅が細い。ノイズ除去効果はあるものの、あらかじめノイズを除去しておくことが、ほぼ必須である。
  Canny(
    image -------- InputArray、8ビット1チャンネル入力画像
    edges -------- OututArray、同上
    threshold1 ---- double、下のしきい値、これ以下はエッジと見なさない
    threshold2 ---- double、上のしきい値、これ以上は必ずエッジとする、中間では、エッジ部分との連結の有無で判断する
    apertureSize = 3 ----- int、使用するSobelカーネルのサイズ
    L2gradient = false ---- bool、時間のかかるL2 norm(二乗和平方根)は使わない、L1 norm(絶対値加算)で実施
  )
 threshold2 / threshold1 = 2 〜 3 が推奨される。
◎Laplacian関数
 二次微分を用いて画像ピクセル値の変化を検出する関数で、僅かな変化にも敏感に反応し、ノイズに弱い。GaussianBlurなどでノイズを除去してから使用するのが原則である。Sobelのように方向性を持たないので、簡単に使用できる。
 Laplacianは二次微分のため、真のエッジ部を挟んで、正負二つの出力が出る。これの絶対値を取ると、エッジが急峻な部分では一本の線になるが、傾斜が緩いエッジでは、線が二本に分かれる。真のエッジは正負の出力の間のゼロクロス点にあるが、これを求めるのは面倒で、エッジの強度にかかわりなく検出される欠点がある。なお、OpenCVにはゼロクロスを求める関数は用意されていない。
  Laplacian(
    src --------- InputArray、入力画像
    dst --------- OutputArray、出力配列
    ddepth ------ int、出力配列の深度、CV_32Fが一般的
    ksize = 1 ---- int、1の場合は3 x 3の標準カーネルを用いる
    scale = 1 ---- double、倍率
    delta = 0 ---- double、オフセット
    borderType = BORDER_DEFAULT ----- int
  )
◎pencilSketch関数
 鉛筆画風にするもので、一種のエッジ検出関数である。しきい値を用いないで、グレイスケールの中間調の線画が得られる。
  pencilSketch(
    src ------- InputArray、3チャンネル入力画像
    dst1 ------ OutputArray、1チャンネル出力画像(グレイスケール)
    dst2 ------ OutputArray、入力画像と同じサイズとタイプの出力画像
    sigma_s = 60 -------- float、範囲は0〜200、目視的には、ほとんど変わらない
    sigma_r = 0.07f ------ float、範囲は0〜1、大きいと明るくなる
    shade_factor = 0.02f -- float、範囲は0〜0.1、大きいと明るくなる
  )
 sigma_rとshade_factorの積が同じであると、明るさはほぼ一定であるが、sigma_rを大きくし過ぎるのは良くない。
◎stylization関数
 これもエッジ検出を応用した関数で、エッジは強調して黒く、エッジ以外の部分は色の分解能を落として(似た色は同色で表す)抽象化し、絵画調にする。
  stylization(
    src ------ InputArray、3チャンネル入力画像
    dst ------ OutputArray、入力画像と同じサイズとタイプの出力画像
    sigma_s = 60 ---- float、範囲は0〜200、目視的には、ほとんど変わらない
    sigma_r = 0.45f --- float、範囲は0〜1、大きいとエッジ(線画状の線)が減る
  )




図1 各種エッジ検出フィルタを比較するプログラム


得られた結果
 図2の上は、原画像とグレイスケール化原画像を示す。図の下は、Sobel関数とScharr関数を用いた結果で、Scharr関数はカーネルの係数が大きいため、出力値が大きいので、しきい値を高くして調整した(Sobelの50に対して、Scharrは190が必要だった)。spatialGradientは、原理上、Sobelと全く同じ結果が得られるので、図としての表示は省略した。
 図3は、原画像に、GaussianBlurによるぼかしグレイスケール原画像を使用しての結果である。左はCanny関数、右は、二次微分タイプのLaplacian関数である(Canny of Gaussian、Laplacian of Gaussianと記してある)。
 図4は、OpenCV3.0になって追加されたNon-Photorealistic Rendaringサブモジュールの関数のpencilSketchとstylizationを試したものである。pencilSketchは鉛筆によるスケッチ風と言われるが、あまり、それらしくない。stylizationは水彩画風と言われるが、それならば、もっと淡い色にすべきであろう。


図2 Sobel関数とScharr関数の比較、ほとんど差はない



図3 CannyとLaplacianは事前のぼかしが必須



図4 エッジ検出を応用した芸術的作品


画像の鮮鋭化
 エッジ検出の強度を弱くしたものが鮮鋭化であると拡大解釈して、画像の鮮鋭化を試みた。OpenCVには、鮮鋭化に特化した関数は用意されていない。自作のカーネルでコンボリューションを実施するか、ボケ画像と原画像の差分を原画像に加算せするunsharpをプログラムで実現するしかない。
 図5はunsharpのプログラムと、OpenCV3.0から加わったdetailEnhance関数のプログラムの要部である。
 unsharp = un + sharpのため、一見、シャープでなくする操作のように感じるが、実際はシャープにする操作である。
 図6は得られた結果で、左下のunsharp画像は、右上のblur原画像に比べると鮮鋭化されているが、左上の原画像には及ばない。図6の右下は、blur原画像にdetailEnhance関数を用いた結果で、コントラスト向上は見られるが、鮮鋭化の実感はない。
◎detailEnhance関数
 エッジには至らない程度の画像上の細かい差を強調して、部分的な詳細をはっきりさせる。
 detailEnhance(
    src ----- InputArray、8ビット3チャンネル画像
    dst ----- OutputArray、srcと同じサイズ、同じタイプの出力画像
    sigma_s = 10 ---- float、範囲は0〜200、大きくするとコントラストが大きくなり、きれいに見える
    sigma_r = 0.15f ---- float、範囲は0〜1、大きくするとエッジが効きすぎる
 )
◎addWeighted関数
 coreモジュールのOperation on Arraysサブモジュールに属する関数で、二つの画像に重みを掛けて加算する。重みをマイナスにすれば、減算になる。
 addWeighted(
    src1 ----- InputArray、第1の入力画像
    alpha ---- double、下記参照
    src2 ----- InputArray、第2の入力画像
    beta ----- double、下記参照
    gamma --- double、下記参照
    dst ------ OutputArray、出力画像
    dtype = -1 ---- int、src2がsrc1と同じ深度であることを意味する
 )
 dst = saturate(alpha * src1 + beta * src2 + gamma)の関係がある。


図5 鮮鋭化(unsharp)と、参考までのdetailEnhance関数を試みる



図6 unsharpでの鮮鋭化とdetailEnhanceでのメリハリ強化


道路標識の場合
 「OpenCVとVisual C++による画像処理と認識(7)」では、エッジ検出を、道路標識認識の前処理と位置づけて取り上げた。ここでも、前資料との比較のために、すでに紹介したプログラムをそのまま用いて、道路標識について確認した。RGB別のエッジ検出はしていない。
 図7によれば、Cannyがノイズが少なく、形状を正しくとらえている。図8は、OpenCV3.0になって新たに追加されたNon-Photorealistic Renderingサブモジュールの関数を道路標識で試したもので、detailEnhanceはノイズが強調され過ぎ、pencilSKetchはお話にならず、道路標識認識の前処理としては向かない。stylizationは漫画のようで、別の用途があるかもしれない。


図7 グレイスケール化した道路標識をエッジ検出した結果



図8 道路標識をNon-Photorealistic Renderingで処理した結果


結 論
 imgprocモジュールのImage Filteringサブモジュールに属する一次微分エッジ検出関数では、Sobelが基本であり、Sobelの縦横をまとめたspatialGradientの結果は、当然、Sobelと全く同じで、Sobelの係数を変えたScharrも、実感的に長所は認められなかった。Scharrは、Sobel関数の引数にCV_SCHARRと記述するだけで実現できるので、別関数にした理由が良く分からない。二次微分エッジ検出関数のLaplacianは、縦横の区別がないので使いやすいが、結果はSobelの方が優れている。imgprocモジュールのFeature DetectionサブモジュールのCannyは素晴らしい。
 鮮鋭化もエッジ検出の弱い場合と拡大解釈して、unsharpを実現した。
 OpenCV3.0になって追加された、photoモジュールのNon-Photorealistic Rendaring(非写実的画像生成)サブモジュールに属する関数は、エッジ検出の応用であると考えて試みたが、非写実性を狙った関数であることは理解できるとしても、まだまだ改良の余地がありそうである。





「Visual C++の勉強部屋」(目次)へ