OpenCV-Python Tutorials (3) ~Image Processing in OpenCV~
OpenCV-Python Tutorialsの記事,Image Processing in OpenCV の章に入ります.
公式:Image Processing in OpenCV — OpenCV-Python Tutorials 1 documentation
色空間の変換
目標
- このチュートリアルでは,BGR Gray,BGR HSV などの画像をある色空間から他の色空間へ変換する方法を学ぶ.
- 加えて,動画の中で色のついた物体を抽出するアプリケーションも作る
- 次の関数を学ぶ:cv2.cvtColor(),cv2.inRange()など
色空間の変換
OpenCV では,150以上の色空間の変換手法を利用可能だ.しかし今回は最も広く使われている,BGR Gray,BGR HSV,の2つだけを見ていく.
色の変換では,cv2.cvtColor(input_image, flag)
という関数を使い,flag
で変換の種類を決める.
BGR Gray の変換では,cv2.COLOR_BGR2GRAY
を,BGR HSV では似たような,cv2.COLOR_BGR2HSV
を使う.他のflagを得るためには,Pythonのターミナルで次のコマンドを叩くだけで良い.
>>> import cv2 >>> flags = [i for i in dir(cv2) if i.startswith('COLOR_')] >>> print(flags)
メモ
HSV では,色相 (Hue) の範囲は[0,179],彩度 (Saturation) の範囲は[0,255],明度 (Value) の範囲は[0,255] となっている.違うソフトウェアでは違うスケールが使われるため,それらで出した値とOpenCVで出した値とを比較する場合は,この範囲に統一する必要がある.
物体のトラッキング
さて BGR 画像から HSV に変換する方法がわかったので,これを色のついた物体を抽出するのに使える.HSV では RGB 色空間より色を表現するのが簡単だ.これから作るアプリケーションでは,青色の物体を抽出を試みる.次に示すのがそのやり方だ.
以下がコード:
import cv2 import numpy as np cap = cv2.VideoCapture(0) while(1): # それぞれのフレームを取得 _, frame = cap.read() # BGR から HSV に変換 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # HSV での青色の範囲を定義 lower_blue = np.array([110, 50, 50]) upper_blue = np.array([130, 255, 255]) # HSVの画像を二値化して青色の物体だけを取得 mask = cv2.inRange(hsv, lower_blue, upper_blue) # マスクと元画像の AND をとる res = cv2.bitwise_and(frame, frame, mask= mask) cv2.imshow('frame',frame) cv2.imshow('mask', mask) cv2.imshow('res', res) k = cv2.waitKey(5) & 0xFF if k == 27: break cv2.destroyAllWindows()
メモ
画像上にいくらかノイズがのっている.後の章で,これらをどうやって取り除くかを見ていく.
メモ
これが物体のトラッキングの最も単純な方法だ.輪郭を取得する関数を学べば,物体の重心を見つけてそれを元に物体をトラッキングする,カメラの前で手を動かすなどによって図形を描くというようなたくさんのことができる.
トラッキングのための HSV の値の探し方
これは stackoverflow.comで一番よくある質問だ.これはとてもシンプルで同じ関数 cv2.cvtColor() を使える.画像を渡す代わりに,欲しい BGR 値を渡す.例えば,緑色の HSV 値を知りたい場合,Pythonのターミナルで以下のようなコマンドを叩けば良い.
>>> green = np.uint8([[[0,255,0 ]]]) >>> hsv_green = cv2.cvtColor(green,cv2.COLOR_BGR2HSV) >>> print(hsv_green) [[[ 60 255 255]]]
これで,最小値と最大値を [H-10, 100,100] と [H+10, 255, 255] にそれぞれ決められる.この手法から離れて, GIMP や値を見つけるためのオンラインでのコンバータのような画像編集ツールを使うことができるが,HSV の範囲に調整することをお忘れなく.
追加資料
練習
- 2色以上の物体を抽出する方法を探してみよう.
画像の幾何学的変換
目標
- 移動,回転,アフィン変換などの異なる幾何学的変換の画像への適用を学ぶ
- cv2.getPerspectiveTransform といった関数を使う
変換
OpenCVは2つの変換の関数を提供している.cv2.warpAffine and cv2.warpPerspectiveの2つだが,これですべての種類の移動ができる.入力として,cv2.warpAffine は 2x3 の変換行列を使い,cv2.warpPerspective は 3x3 の変換行列を使う.
拡大縮小
拡大縮小は画像をリサイズするだけだ.OpenCVでは cv2.resize() 関数がその目的に対応するものだ.画像のサイズを手動で決めるか,拡大縮小の要素を設定すれば良い.他の補完手法が使われる.縮小に対しては cv2.INTER_AREA,拡大に対しては cv2.INTER_CUBIC (遅い) と cv2.INTER_LINEAR がより望ましい補完手法だ.デフォルトでは,補完手法はすべての拡大・縮小の目的に対して cv2.INTER_LINEAR が使われている.入力画像を以下のいずれかの方法で拡大・縮小できる.
In [2]: import cv2 In [3]: import numpy as np In [4]: img = cv2.imread('messi.jpg') In [5]: res = cv2.resize(img, None, fx=2, fy=2, interpolation = cv2.INTER_CUBIC) # または In [7]: height, width = img.shape[:2] In [8]: res = cv2.resize(img, (2*width, 2*height), interpolation = cv2.INTER_CUBIC)
移動
移動(Translation)は対象物の位置を動かす処理だ. 方向の移動がわかっている場合,それを と置くと,次のような変換行列 を作れる.
これをnp.float32
型のNumpyのarrayに入れて,cv2.warpAffine() 関数に渡す.以下が (100, 50) 移動の場合のコードだ.
In [3]: import cv2 In [4]: import numpy as np In [5]: img = cv2.imread('mess') messi.jpg messi.png In [5]: img = cv2.imread('messi.jpg', 0) In [6]: rows, cols = img.shape In [7]: M = np.float32([[1,0,100],[0,1,50]]) In [8]: dst = cv2.warpAffine(img,M,(cols,rows)) In [9]: cv2.imshow('img',dst) In [10]: cv2.waitKey(0) Out[10]: 46 In [11]: cv2.destroyAllWindows()
注意
cv2.warpAffine() 関数の3つ目の引数は出力画像のサイズで,(width, height) の形をとる.
結果は下のようになった.
回転
角度の回転は次の変換行列でできる
しかし,OpenCVはどの位置にあっても回転できるように回転中心を調整する回転を提供している.この変換行列は次のようになる.
ただし,
変換行列を探すためには,OpenCVはcv2.getRotationMatrix2Dという関数を提供している.次の例は拡大・縮小なしで画像を90度回転させたものだ.
In [14]: img = cv2.imread('messi.jpg', 0) In [15]: rows, cols = img.shape In [16]: M = cv2.getRotationMatrix2D((cols/2,rows/2), 90, 1) In [17]: dst = cv2.warpAffine(img, M, (cols, rows)) In [18]: cv2.imshow('img',dst) In [19]: cv2.waitKey(0)
結果は次のようになった.
アフィン変換
アフィン変換では,元画像で平行な線は出力画像でも平行のままとなる.変換行列を決めるために,入力画像上の3点と出力画像に対応する位置が必要だ.cv2.getAffineTransform はcv2.warpAffineに渡す 2x3 の行列を作成する.
下の例を見てみよう.そして,選択された(緑で印が付けられている)点も見てみよう.
In [1]: import cv2 In [2]: import numpy as np In [3]: import matplotlib.pyplot as plt In [4]: img = cv2.imread('image.png') In [5]: rows, cols, ch = img.shape In [6]: pts1 = np.float32([[30,30],[100,30],[30,100]]) In [7]: pts2 = np.float32([[10,70],[100,30],[70,150]]) In [8]: M = cv2.getAffineTransform(pts1,pts2) In [9]: dst = cv2.warpAffine(img,M,(cols,rows)) #選択された点に印をつける In [10]: img = cv2.circle(img,(30,30), 3, (0,255,0), -1) In [11]: img = cv2.circle(img,(100,30), 3, (0,255,0), -1) In [12]: img = cv2.circle(img,(30,100), 3, (0,255,0), -1) In [13]: dst = cv2.circle(dst,(10,70), 3, (0,255,0), -1) In [14]: dst = cv2.circle(dst,(100,30), 3, (0,255,0), -1) In [15]: dst = cv2.circle(dst,(70,150), 3, (0,255,0), -1) In [16]: plt.subplot(121),plt.imshow(img),plt.title('Input') Out[16]: (<matplotlib.axes._subplots.AxesSubplot at 0x10778c0f0>, <matplotlib.image.AxesImage at 0x10a929518>, <matplotlib.text.Text at 0x10a909320>) In [17]: plt.subplot(122),plt.imshow(dst),plt.title('Output') Out[17]: (<matplotlib.axes._subplots.AxesSubplot at 0x10a93ec18>, <matplotlib.image.AxesImage at 0x10a987be0>, <matplotlib.text.Text at 0x10a96b7f0>) In [18]: plt.show()
結果は次のようになった.
透視変換
透視変換には,3x3の変換行列が必要だ.この変換では直線は直線のまま残る.この変換行列を決めるためには,入力画像上の4点と対応する出力画像上の点が必要となる.これら4点のうち,3点は共線(3点が同一直線上にあること)であってはならない.そして,cv2.getPerspectiveTransform関数によって変換行列が決められ,cv2.warpPerspectiveに渡す.
以下のコードを見てみよう.
In [1]: import cv2 In [2]: import numpy as np In [3]: import matplotlib.pyplot as plt In [4]: img = cv2.imread('goBoard.jpg') In [5]: rows, cols. ch = img.shape # 4点と中心線を描く In [8]: img = cv2.circle(img,(67,35),5,(0,255,0),-1) In [9]: img = cv2.circle(img,(430,36),5,(0,255,0),-1) In [10]: img = cv2.circle(img,(14,416),5,(0,255,0),-1) In [11]: img = cv2.circle(img,(482,417),5,(0,255,0),-1) In [12]: img = cv2.line(img,(0,250), (513,250),(0,255,0),2) In [13]: img = cv2.line(img,(256,0), (256,500),(0,255,0),2) In [14]: pts1 = np.float32([[67,35],[430,36],[14,416],[482,417]]) In [15]: pts2 = np.float32([[0,0],[513,0],[0,500],[513,500]]) In [16]: M = cv2.getPerspectiveTransform(pts1,pts2) In [17]: dst = cv2.warpPerspective(img,M,(513,500)) In [18]: plt.subplo plt.subplot plt.subplot2grid plt.subplot_tool plt.subplots plt.subplots_adjust In [18]: plt.subplot(121),plt.imshow(img),plt.title('Input') Out[18]: (<matplotlib.axes._subplots.AxesSubplot at 0x10b1450b8>, <matplotlib.image.AxesImage at 0x10e1679b0>, <matplotlib.text.Text at 0x10e13de10>) In [19]: plt.subplot(122),plt.imshow(dst),plt.title('Output') Out[19]: (<matplotlib.axes._subplots.AxesSubplot at 0x10e179160>, <matplotlib.image.AxesImage at 0x110295748>, <matplotlib.text.Text at 0x110270da0>) In [20]: plt.show()
結果:
なんだこの色は…
おまけ
なんか変な色になったのでinputの画像を出してみた
In [21]: cv2.imwrite('input.jpg',im) image.png img import In [21]: cv2.imwrite('input.jpg',img) Out[21]: True
どこで変わったんだろう…
追加資料
- “Computer Vision: Algorithms and Applications”, Richard Szeliski
画像の二値化
目標
このチュートリアルでは,Simple thresholding,Adaptive thresholding,Otsu’s thresholding を学ぶ
次の関数を学ぶ:cv2.threshold,cv2.adaptiveThreshold
Simple Thresholding
ピクセル値が閾値以上であれば1,そうでなければ0を割り当てる.使われる関数は cv2.threshold で,最初の引数はソースの画像,2番目の引数はピクセル値を分類するための閾値,3番目の引数はピクセル値が閾値より大きい(または小さい)値になる時に与えられる値を表す最大値だ.OpneCVは異なる種類の二値化を提供していて,そのどれであるかは関数の4番目のパラメータによって決まる.その種類は,
- cv2.THRESH_BINARY
- cv2.THRESH_BINARY_INV
- cv2.THRESH_TRUNC
- cv2.THRESH_TOZERO
- cv2.THRESH_TOZERO_INV
ドキュメントは明確にそれぞれのタイプにどんな意味があるかを説明している.ドキュメントを参照しよう.
2つの出力が含まれている.1つ目は後に説明する戻り値だ.2つ目は二値化された画像だ.
コード:
#!/usr/bin/python # _*_ coding: utf-8 -*- import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('gradient.png', 0) ret,thresh1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) ret,thresh2 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV ) ret,thresh3 = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC) ret,thresh4 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO) ret,thresh5 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV) titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV'] images = [img, thresh1, thresh2, thresh3, thresh4, thresh5] for i in range(6): plt.subplot(2, 3, i+1),plt.imshow(images[i], 'gray') plt.title(titles[i]) plt.xticks([]), plt.yticks([]) plt.show()
メモ
多数の画像をプロットするために,関数 plt.subplot() を使った.詳細は, Matplotlib のドキュメントを参照しよう.
結果は次のようになる:
Adaptive Thresholding
前の節では,二値化の閾値としてグローバル値を用いた.しかし,別のエリアでは画像は異なる照明条件を持つような全ての状況でこのやり方は良くないかもしれない.その場合,adaptive thresholding を使おう.この方法ではアルゴリズムが画像の小領域へ二値化してくれる.従って同じ画像の異なる領域に異なった二値化をしてくれ,照度が変化している画像でもより良い結果を得ることができる.
3つの特別な入力パラメータと1つの出力引数を持つ.
Adaptive Method - 閾値をどのように計算するかを決める
- cv2.ADAPTIVE_THRESH_MEAN_C : 閾値は近くのエリアの平均.
- cv2.ADAPTIVE_THRESH_GAUSSIAN_C : 閾値はガウス窓によって重み付けをした近くの値の合計で重み付けされた値である.
Block Size - 近くのエリアの大きさを決める
C - 平均や重み付けされた平均から引かれる定数
次のコードで照度が変化している画像に対して global thresholding と adaptive thresholding を比べる.
#!/usr/bin/python # _*_ coding: utf-8 -*- import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('dave.jpg', 0) img = cv2.medianBlur(img, 5) ret, th1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) th2 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2) th3 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) titles = ['Original Image', 'Global Thresholding (v = 127)', 'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding'] images = [img, th1, th2, th3] for i in range(4): plt.subplot(2,2,i+1), plt.imshow(images[i], 'gray') plt.title(titles[i]) plt.xticks([]), plt.yticks([]) plt.show()
結果:
Otsu's Binarization
最初の説で,2つ目のパラメータ retVal があると述べた.これは Otsu's Binarization の時に使う.
グローバルな二値化では,閾値として任意の値を使った.じゃあその選ばれたものが良い値か悪い値かはどうやって知るのだろう?答えは,試行錯誤によるやり方だ.しかし,bimodel image(簡単に言うと,bimodel image は2つの峰を持った画像だ)について考えてみよう.その画像では,それらの峰の中間値を閾値として大まかに取れそうではないか?これが Otsu binarization がすることだ.従って簡単に言うと,これは bimodel image に対して画像のヒストグラムから閾値を大まかに計算する.(bimodel image でない画像に対しては,二値化は正確でないだろう.)
このように関数 cv2.threshold() は使われるが,cv2.THRESH_OTSU という追加のフラグを渡す.閾値としては単純に0を渡す.そうするとアルゴリズムが最適な閾値を見つけて2つ目の出力retVal
として返してくれる.
次の例を見てみよう.入力画像は雑音画像だ.1つ目は,閾値を127としてグローバルな二値化を使った.2つ目は,Otsu's thresholding を直接使った.3つ目は,5x5のガウスカーネル(gaussian kernel)で画像にフィルターをかけた後,Otsu's thresholding を使った.ノイズフィルタリングがどれぐらい結果を良くしたかを見てみよう.
#!/usr/bin/python # _*_ coding: utf-8 -*- import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('noisy.png', 0) # global thresholding ret1, th1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) # Otsu's thresholding ret2, th2 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU) # Otsu's thresholding after Gaussian filtering blur = cv2.GaussianBlur(img, (5,5,), 0) ret3 ,th3 = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU) # plt all the images and their histgrams images = [img, 0, th1, img, 0, th2, blur, 0, th3] titles = ['Original Noisy Image', 'Histgram', 'Global Thresholding (v=127)', 'Original Noisy Image', 'Histgram', "Otsu's Thresholding", 'Gaussian filterd Image', 'Histgram', "Otsu's Thresholding"] for i in range(3): plt.subplot(3,3,i*3+1),plt.imshow(images[i*3], 'gray') plt.title(titles[i*3]), plt.xticks([]), plt.yticks([]) plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(), 256) plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([]) plt.subplot(3, 3, i*3+3), plt.imshow(images[i*3+2], 'gray') plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([]) plt.show()
結果:
省略
(双峰性を持つ画像が見つからなかったので試せてない)
Otsu's Binarization の仕組み
この節では Otsu's binarization のPythonの実行で実際にどうやって動いているかを説明する.興味がないならスキップしても大丈夫.
二値化画像を扱っているので,Otsu's algorithm は次の式で与えられる重み付けされた級内分散を最小にする閾値を見つけるものだ.
ここで,
&
&
これによって両方のクラスの分散が最小となるような二つのピークの間にあるような値を見つける.これはPythonで次のように簡単に実行できる.
img = cv2.imread('noisy2.png',0) blur = cv2.GaussianBlur(img,(5,5),0) # find normalized_histogram, and its cumulative distribution function hist = cv2.calcHist([blur],[0],None,[256],[0,256]) hist_norm = hist.ravel()/hist.max() Q = hist_norm.cumsum() bins = np.arange(256) fn_min = np.inf thresh = -1 for i in range(1,256): p1,p2 = np.hsplit(hist_norm,[i]) # probabilities q1,q2 = Q[i],Q[255]-Q[i] # cum sum of classes b1,b2 = np.hsplit(bins,[i]) # weights # finding means and variances m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2 v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2 # calculates the minimization function fn = v1*q1 + v2*q2 if fn < fn_min: fn_min = fn thresh = i # find otsu's threshold value with OpenCV function ret, otsu = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) print thresh,ret
(ここでいくつか新しく出てきた関数があるが,それらはこれからの章で説明する)
追加資料
- Digital Image Processing, Rafael C. Gonzalez
練習
- Otsu’s binarization での最適化がいくつか存在する.調べて実行してみよう.