読者です 読者をやめる 読者になる 読者になる

OpenCV-Python Tutorials (5) ~Image Processing in OpenCV~

OpenCV OpenCV-Python Tutorials Tutorial プログラミング

OpenCV-Python Tutorialsの記事,Image Processing in OpenCV の章の Morphological Transformations, Image Gradients,Image Pyramids を見ていく.

公式:Image Processing in OpenCV — OpenCV-Python Tutorials 1 documentation

この中で試したコードはGitHubに置いておくことに.

github.com

画像の勾配

目標

この章では,次のことを学ぶ

  • 画像の勾配,エッジなどを見つける
  • 次のような関数を扱う:cv2.Sobel(), cv2.Scharr(), cv2.Laplacian()など
理論

OpenCVは3種類の勾配フィルタまたはハイパスフィルタ,Sobel, Scharr, Laplacian を提供している.それぞれ見ていこう.

1. Sobel and Scharr Derivatives

Sobelの演算はガウシアン平滑化と微分演算の組み合わせたもので,よりノイズに強い.取るべき導関数の方向を,水平方向か垂直方向か(引数はそれぞれyorderxorder)を指定することができる.引数ksizeによってカーネルの大きさも指定することができる.もし,ksize=-1なら,3x3のSobelフィルタより良い結果を出す3x3のScharrフィルタが使われる.使われているフィルタはドキュメントで確認しよう.

2. Laplacian Derivatives

与えられた画像のラプラシアンを次の関係で計算する:.これはSobel導関数を用いて見つけたそれぞれの導関数である.ksize = 1なら,次のカーネルがフィルタとして使われる:

コード

次のコードは一つの図形に対してのすべての演算処理を見せる.カーネルの大きさはすべて5x5だ. np.uint8型の結果を得るのに,出力画像の depth は-1が渡される.

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

img = cv2.imread('dave.jpg',0)

laplacian = cv2.Laplacian(img, cv2.CV_64F)
sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)

plt.subplot(2, 2, 1), plt.imshow(img, cmap = 'gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 2), plt.imshow(laplacian, cmap = 'gray')
plt.title('Laplacian'), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 3), plt.imshow(sobelx, cmap = 'gray')
plt.title('Sobel X'), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 4), plt.imshow(sobely, cmap = 'gray')
plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])

plt.show()

結果: f:id:asdm:20160217214246p:plain

大事な問題

さっきの例では,出力のデータ型は cv2.CV_8U または np.uint8 だが,少し問題がある.黒から白への変化は正の傾き(正の値)として捉えられる一方,白から黒への変化は負の傾き(負の値)と捉えられる.従って,np.uint8 にデータを変換するときには,負の傾きのものはすべて0とされてしまう.言い換えると,エッジを見落としてしまう.

もし,どちらのエッジも検出するには,出力のデータ型を cv2.CV_16S, cv2.CV_64F などのような高いレベルのもので保持する処理をして,絶対値をとって cv2.CV_8U に戻す変換をするのが良い.下のコードは水平方向のSobelフィルタに対してこの処理をして,結果の差を見るものだ.

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

img = cv2.imread('box.png', 0)

# Output dtype = cv2.CV_8U
sobelx8u = cv2.Sobel(img, cv2.CV_8U, 1, 0, ksize=5)

# Output dtype = cv2.CV_64F. Then take its absolute and convert to cv2.CV_8U
sobelx64f = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
abs_sobel64f = np.absolute(sobelx64f)
sobel_8u = np.uint8(abs_sobel64f)

plt.subplot(1, 3, 1), plt.imshow(img, cmap = 'gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(1, 3, 2), plt.imshow(sobelx8u, cmap = 'gray')
plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
plt.subplot(1, 3, 3), plt.imshow(sobel_8u, cmap = 'gray')
plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])

plt.show()

結果: f:id:asdm:20160217223949p:plain

Cannyエッジ検出

目標

この章では,次のことについて学ぶ

  • Cannyエッジ検出のコンセプト
  • OpenCVの関数:cv2.Canny()
理論

Cannyエッジ検出は有名なエッジ検出のアルゴリズムだ.1986年にJohn F. Cannyによって発明された.これは複数段階のアルゴリズムだが,それぞれについて見ていこう.

1. ノイズ処理

画像上のノイズにエッジ検出は影響されやすいため,最初のステップでは5x5のガウシアンフィルタを使ってノイズを取り除く.これについては前の章で既にみた.

2. 画像の勾配強度を見つける

水平方向({G_x})と垂直方向({G_y})の1次導関数を求めるために,平滑化された画像に水平と垂直の両方向へSobelカーネルでフィルターをかける.これら二つの画像から,それぞれのピクセルに次のような処理をすることでエッジの勾配と方向を知ることができる.

勾配の方向は常にエッジに対して垂直である.これは垂直,水平,2つの対角線の4つの方向の中の1つに近似される.

3. Non-maximal suppression(局所的に最大値以外を0に抑える)

勾配の大きさと方向を取得した後,エッジだと考えられていないピクセルを除く処理を画像全体に対して行う.このために,すべてのピクセルに対して,勾配方向の近傍で局所的に最大かどうかを確かめる.下の画像を見てみよう:

f:id:asdm:20160222131541j:plain

点Aは(垂直方向の)エッジ上にあり,勾配の方向はエッジに対して垂直だ.点BとCは勾配方向の直線上にある.したがって,点BとCを使って局所的に最大かを見ることで点Aを確かめることができる.もしそうならば次の段階を考えられ,そうでないならば抑えられる(0に置き換えられる).

要するに,得られる結果は"細いエッジ"の二値化画像だ.

4. Hysteresis Thresholding

この段階では,どれが本当のエッジでどれがそうでないかを決める.このため,minValmaxVal の2つの閾値が必要となる.勾配の強度が maxVal よりも大きいものはエッジで minVal より小さいものはエッジではないとして破棄される.2つの閾値の間にあるものは連結度に基づいてエッジであるかエッジでないかに分類される.もし,"確かなエッジ"につながっていれば,エッジの一部であると考えられる.そうでなければ,破棄される.次の図を見てみよう:

f:id:asdm:20160222142839j:plain

エッジAは maxVal を上回っていて,"確かなエッジ"であると考えられる.エッジCは maxValを下回っているが,エッジAにつながっているので正当なエッジであると考えられ,カーブ全体を得る.しかし,エッジBは minVal を上回っているがエッジCと同じ範囲にあり,"確かなエッジ"につながっていないため破棄される.したがって,正しい結果を得るために minValmaxVal を適切に選択す必要があり,とても重要だ.

この段階ではまたエッジは長い線であるという仮定に基づき,小さなピクセルのノイズを取り除く.

よって,最後に得られるものは画像上の強いエッジだ.

OpenCVでのCannyエッジ検出

OpenCVは上で述べたものすべてを一つの処理として,cv2.Canny() を用意している.どのように使うかを見ていこう.最初の引数は入力画像,2番目と3番目の引数はそれぞれ minValmaxVal だ.3番目の引数はSobelオペレータのアパーチャサイズで,デフォルトでは3になっている.最後の引数は勾配の大きさを見つけるための平均化を表す L2gradient だ.もしこれがTrueなら,より正確な平均化が使われるが,そうでなければ次の関数が使われる:{Edge \_ Gradient(G) = |G_x| + |G_y|}.デフォルトではFalseである.

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

img = cv2.imread('messi.jpg', 0)
edges = cv2.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()

結果: f:id:asdm:20160222165235p:plain

追加資料

次のサイトとかを参考にしました

  1. エッジ検出 - Wikipedia

  2. コンピュータビジョンのセカイ - 今そこにあるミライ (40) iPhoneアプリ「漫画カメラ」で使われている画像処理手法その1 | マイナビニュース

練習
  1. 閾値を2つのトラックバーを使って様々な値にできるCannyエッジ検出機のアプリケーションを作ろう.そうすることで閾値の効果を理解出来る.

Image Pyramids

目標

この章では,

  • Image Pyramidsについて学ぶ
  • 新しいフルーツ"Orapple"を作るためにImage Pyramidsを使う
  • 次の関数を見ていく:cv2.pyrUp(), cv2.pyrDown()
理論

通常,一定の大きさの画像を扱ってきた.しかし幾つかの場合,私たちは同じ画像の異なる解像度の画像を扱う必要がある.例えば,画像上の顔などの何かを探す時,画像上でどの大きさで現れるかはわかっていない.この場合,異なる解像度の画像セットを作って画像上でその対象物を探す必要がある.この異なる解像度の画像セットのことをPyramidsと呼ぶ(なぜならスタックにこれらを格納する時,一番大きな画像が下で一番小さな画像がトップにくるのがピラミッドのように見えるからだ).

2種類のImage Pyramidsがある.1) Gaussian Pyramid と 2) Laplacian Pyramids だ.

Gaussian Pyramid の高いレベル(低い解像度)は低いレベル(高い解像度)の画像の中の連続する行と列を削除することによって形成される.そして高いレベルのそれぞれのピクセルは下のレベルの5ピクセルをガウシアンの重み付けをした contribution from によって形成される.そうすることでM \times Nの画像がM/2 \times N/2の画像になる.よって,面積は4分の1に減少する.これは Octave と呼ばれる.ピラミッドの上に向かって同じパターンを続ける(すなわち,解像度は減少していく).同じように拡張する場合は,面積はそれぞれのレベルで4倍になる.cv2.pyrDown()cv2.pryUp()関数を使って Gaussian Pyramid を見つけることができる.

img = cv2.imread('messi.jpg')
lower_reso = cv2.pyrDown(higher_reso)

下のものが image pyramid の4段階だ.

f:id:asdm:20160222215227j:plain

cv2.pyrUp()関数で image pyramid を下に進むこともできる.

higher_reso2 = cv2.pyrUp(lower_reso)

higher_reso2higher_zero と同じではないことに注意しよう,なぜなら一度解像度を下げると,情報は失われてしまうからだ.次の画像は,前に示した一番小さい画像からつくられたピラミッドを3段階降りたものだ.元の画像と比較しよう:

f:id:asdm:20160222220300j:plain

Laplacian Pytamids は Gaussian Pyramids から生成される.そのための関数は存在しない.Laplacian pyramid の画像はエッジ画像のようなものだけで,多くの要素はゼロだ.これらは画像圧縮で使われる.Laplacian Pyramid のあるレベルは Gaussian Pyramid のレベル間の違いと Gaussian Pyramid の上のレベルの拡張したものによって作られる.Laplacian レベルの3段階は次のようになる(コントラストはコンテンツをよりよくするために調整してある):

f:id:asdm:20160222222253j:plain
ピラミッドを使った画像の合成

ピラミッドの1つのアプリケーションが画像の合成だ.例えば,画像のつなぎ合わせでは,2つの画像を積み重ねる必要があるが,画像の間の不連続性によっていい感じに見れないかもしれない.この場合,ピラミッドを使った画像の合成は画像上の多くのデータを失わずにシームレスな合成が出来る.1つの古典的な例がオレンジとリンゴの2つの果物の合成だ.何を言おうとしているかを理解するために結果を見よう:

f:id:asdm:20160222224328j:plain

追加資料の最初のリファレンスには,Laplacian Pyramids などの図表を使った画像の合成(image blending)の説明がされているのでチェックしておこう.簡単な処理の流れは以下のとおりだ:

  1. りんごとオレンジの2つの画像を読み込む
  2. りんごとオレンジの Gaussian Pyramids を求める(この実践例では,レベルは6だ)
  3. Gaussian Pyramids から,その Laplacian Pyramids を求める.
  4. りんごの左半分とオレンジの右半分を Laplacian Pyramids のそれぞれのレベルでつなげる
  5. 最後にこのつなげた image pyramids から元の画像を再構築する

下がコード全体だ.(簡単のため,余分なメモリをとってそれぞれの段階を分けて行っている.やりたかったら最適化できる.)

import cv2
import numpy as np,sys

A = cv2.imread('apple.jpg')
B = cv2.imread('orange.jpg')

G = A.copy()
gpA = [G]
for i in xrange(6):
  G = cv2.pyrDown(gpA[i])
  gpA.append(G)

G = B.copy()
gpB = [G]
for i in xrange(6):
  G = cv2.pyrDown(gpB[i])
  gpB.append(G)

lpA = [gpA[5]]
for i in xrange(5, 0, -1):
  size = (gpA[i-1].shape[1], gpA[i-1].shape[0])
  GE = cv2.pyrUp(gpA[i], dstsize = size)
  L = cv2.subtract(gpA[i-1], GE)
  lpA.append(L)

lpB = [gpB[5]]
for i in xrange(5, 0, -1):
  size = (gpB[i-1].shape[1], gpB[i-1].shape[0])
  GE = cv2.pyrUp(gpB[i], dstsize = size)
  L = cv2.subtract(gpB[i-1], GE)
  lpB.append(L)

LS = []
for la,lb in zip(lpA, lpB):
  rows, cols, dpt = la.shape
  ls = np.hstack((la[:,0:cols/2], lb[:,cols/2:]))
  LS.append(ls)

ls_ = LS[0]
for i in xrange(1,6):
  size = (LS[i].shape[1], LS[i].shape[0])
  ls_ = cv2.pyrUp(ls_, dstsize = size)
  ls_ = cv2.add(ls_, LS[i])

real = np.hstack((A[:,:cols/2],B[:,cols/2:]))

cv2.imwrite('Pyramid_blending2.jpg',ls_)
cv2.imwrite('Direct_blending.jpg',real)
追加資料
  1. Image Blending

    載ってたコードの通り動かしたらエラーが出たけど,stackoverflow で対処法見つけた

  2. python - opencv error:Sizes of input arguments do not match - Stack Overflow