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 \leftrightarrow Gray,BGR \leftrightarrow HSV などの画像をある色空間から他の色空間へ変換する方法を学ぶ.
  • 加えて,動画の中で色のついた物体を抽出するアプリケーションも作る
  • 次の関数を学ぶ:cv2.cvtColor()cv2.inRange()など

色空間の変換

OpenCV では,150以上の色空間の変換手法を利用可能だ.しかし今回は最も広く使われている,BGR \leftrightarrow Gray,BGR \leftrightarrow HSV,の2つだけを見ていく.

色の変換では,cv2.cvtColor(input_image, flag)という関数を使い,flagで変換の種類を決める.

BGR \leftrightarrow Gray の変換では,cv2.COLOR_BGR2GRAYを,BGR \leftrightarrow 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 色空間より色を表現するのが簡単だ.これから作るアプリケーションでは,青色の物体を抽出を試みる.次に示すのがそのやり方だ.

  • 動画のそれぞれのフレームを取得する
  • BGR から HSV 色空間に変換する
  • 青色の範囲で HSV 画像を閾値で判定する
  • これで青い物体のみを抽出できたので,その画像を好きなようにできる

以下がコード:

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 の範囲に調整することをお忘れなく.

追加資料

練習
  1. 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)は対象物の位置を動かす処理だ.(x,y) 方向の移動がわかっている場合,それを (t_x , t_y) と置くと,次のような変換行列 M を作れる.

f:id:asdm:20151210123943p:plain

これを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) の形をとる.

結果は下のようになった.

f:id:asdm:20151210153421p:plain

回転

角度\thetaの回転は次の変換行列でできる

f:id:asdm:20151210155701p:plain

しかし,OpenCVはどの位置にあっても回転できるように回転中心を調整する回転を提供している.この変換行列は次のようになる.

f:id:asdm:20151210161055p:plain

ただし,

\alpha = scale \cdot cos \theta\beta = scale \cdot sin \theta

変換行列を探すためには,OpenCVcv2.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)

結果は次のようになった. f:id:asdm:20151210171421p:plain

アフィン変換

アフィン変換では,元画像で平行な線は出力画像でも平行のままとなる.変換行列を決めるために,入力画像上の3点と出力画像に対応する位置が必要だ.cv2.getAffineTransformcv2.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()

結果は次のようになった. f:id:asdm:20151210185515p:plain

透視変換

透視変換には,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()

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

なんだこの色は…

おまけ

なんか変な色になったのでinputの画像を出してみた

In [21]: cv2.imwrite('input.jpg',im)
image.png  img        import     

In [21]: cv2.imwrite('input.jpg',img)
Out[21]: True

f:id:asdm:20151211142925j:plain

どこで変わったんだろう…

追加資料

  1. Computer Vision: Algorithms and Applications”, Richard Szeliski

画像の二値化

目標
  • このチュートリアルでは,Simple thresholding,Adaptive thresholding,Otsu’s thresholding を学ぶ

  • 次の関数を学ぶ:cv2.thresholdcv2.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 のドキュメントを参照しよう.

結果は次のようになる: f:id:asdm:20160110161353p:plain

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()

結果:

f:id:asdm:20160110171821p:plain

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 は次の式で与えられる重み付けされた級内分散を最小にする閾値を見つけるものだ.

\sigma_\omega^2 (t) = q_1(t)\sigma_1^2 + q_2(t)\sigma_2^2

ここで,

q_1(t) = \sum_{i=1}^{t} P(i) & q_1(t) = \sum_{i=t+1}^{I}
\mu_1(t) = \sum_{i=1}^{t} \frac{iP(i)}{q_1(t)} &  \mu_2(t) = \sum_{i=t+1}^{I} \frac{iP(i)}{q_2(t)}
\sigma_1^2(t) = \sum_{i=1}^{t} [i-\mu_1(t)]^2 \frac{iP(i)}{q_1(t)} & \sigma_1^2(t) = \sum_{i=t+1}^{I} [i-\mu_1(t)]^2 \frac{iP(i)}{q_2(t)}

これによって両方のクラスの分散が最小となるような二つのピークの間にあるような値を見つける.これは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

(ここでいくつか新しく出てきた関数があるが,それらはこれからの章で説明する)

追加資料

  1. Digital Image Processing, Rafael C. Gonzalez
練習
  1. Otsu’s binarization での最適化がいくつか存在する.調べて実行してみよう.