【Python】OpenCV3で画像処理を学ぶ (1)

OpenCV3を使って画像処理の基礎を勉強中です。参考書籍は, 『OpenCVによる画像処理入門』です。

まず, 本書の定義を用いて 画像処理 と コンピュータビジョン の定義を分けておく。

  • 画像処理: 与えられた画像に対して, なんらかの処理を行い画像を出力する処理
  • コンピュータビジョン(CV): 撮影対象を認識/判別したり, 対象の状態をデータで出力する処理

今回は, 基本的な画像処理技術 (特に 色空間変換, 幾何学変換, 濃淡変換) を OpenCV3 で試してみた。

実行環境

環境は macOS Sierra, Python 2.7.12。

Sierraの環境では brew によるインストール時 `QTKit not found` で上手くいかなかったが `–HEAD` オプションを追加すると上手くいった。
Python環境をちゃんと分けたい場合は注意が必要。

$ brew update
$ brew tap homebrew/science
$ brew install cmake pkg-config jpeg libpng libtiff eigen openexr numpy tbb ffmpeg
$ brew install opencv3 --HEAD --with-python3 --with-ffmpeg --with-tbb --with-contrib
$ brew link opencv3

Versionを確認。

In [1]: import cv2

In [2]: cv2.__version__
Out[2]: '3.1.0-dev'

画像情報の取得

まずは, 画像データを読み込んで 幅/高さ/深度 を取得してみる。
画像は共通して, Lenna.jpg を使い, 読み込んだ画像データは本文中で img_src として統一する。

In [3]: img_src = cv2.imread('./images/Lenna.jpg', cv2.IMREAD_UNCHANGED)

In [4]: height, width, channels = img_src.shape[:3]

In [5]: 'width: %s, height: %s, channels: %s, dtype: %s' % (str(width), str(height), s
   ...: tr(channels), str(img_src.dtype))
Out[6]: 'width: 400, height: 400, channels: 3, dtype: uint8'

Lenna

画像の入出力を扱う imgcodecsモジュール では, 様々なフォーマットの入出力をサポートしている。[1]
代表的な関数として cv2.imread(), cv2.imwrite() がある。

cv2.imread(filename[, flags]) で指定できる flags はヘッダ内で宣言されていた。

// modules/imgcodecs/include/opencv2/imgcodecs.hpp
enum ImreadModes {
       IMREAD_UNCHANGED            = -1, //!< If set, return the loaded image as is (with alpha channel, otherwise it gets cropped).
       IMREAD_GRAYSCALE            = 0,  //!< If set, always convert image to the single channel grayscale image.
       IMREAD_COLOR                = 1,  //!< If set, always convert image to the 3 channel BGR color image.
       IMREAD_ANYDEPTH             = 2,  //!< If set, return 16-bit/32-bit image when the input has the corresponding depth, otherwise convert it to 8-bit.
       IMREAD_ANYCOLOR             = 4,  //!< If set, the image is read in any possible color format.
       IMREAD_LOAD_GDAL            = 8,  //!< If set, use the gdal driver for loading the image.
       IMREAD_REDUCED_GRAYSCALE_2  = 16, //!< If set, always convert image to the single channel grayscale image and the image size reduced 1/2.
       IMREAD_REDUCED_COLOR_2      = 17, //!< If set, always convert image to the 3 channel BGR color image and the image size reduced 1/2.
       IMREAD_REDUCED_GRAYSCALE_4  = 32, //!< If set, always convert image to the single channel grayscale image and the image size reduced 1/4.
       IMREAD_REDUCED_COLOR_4      = 33, //!< If set, always convert image to the 3 channel BGR color image and the image size reduced 1/4.
       IMREAD_REDUCED_GRAYSCALE_8  = 64, //!< If set, always convert image to the single channel grayscale image and the image size reduced 1/8.
       IMREAD_REDUCED_COLOR_8      = 65  //!< If set, always convert image to the 3 channel BGR color image and the image size reduced 1/8.
     };

読み込んだ画像データの class は numpy.ndarray なのでゼロから生成するのも難しくない。

In [2]: import numpy as np

In [3]: cols = 480

In [4]: rows = 640

In [5]: img_blk = np.zeros((rows, cols, 3), np.uint8)

In [6]: height, width, channels = img_blk.shape[:3]

In [7]: 'width: %s, height: %s, channels: %s, dtype: %s' % (str(width), str(height), s
   ...: tr(channels), str(img_blk.dtype))
Out[7]: 'width: 480, height: 640, channels: 3, dtype: uint8'

読み込んだ画像データに対して以下の変換を行う。

  • 色空間変換
  • 幾何学変換
  • 濃淡変換

色空間変換

色空間は, 色を定量化するために立方的に記述された色の空間で, 様々な色空間がある。一般的な色空間として以下がある。

  • RGB: Red, Green, Blue の3色を組み合わせて色彩を表現。加法混色。
  • YUV: 輝度信号Yと2つの色差信号 (Uは青-黄成分, Vは赤-緑成分) で表現する。映像信号用。YCbCrとは色差信号の振幅が異なる。
  • HSV: 色相(Hue)、彩度(Saturation)、明度(Value) で表現。

色空間の変換では事前に値域は正規化, または 0-255 に収まるような変換をしておく。

OpenCVでは cv2.cvtColor(src, code[, dst[, dstCn]]) で色空間の変換が行える。RGBからHSVへ変換してみる。

In [8]: img_hsv = cv2.cvtColor(img_src, cv2.COLOR_BGR2HSV)

cv2-bgr2hsv

RGBからグレースケール画像への変換は `cv2.COLOR_BGR2GRAY` を指定する。

In [10]: img_gry = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)

cv2-bgr2gray

幾何学変換

幾何学変換 とは座標に関する変換。 幾何学変換の中で (x,y) の座標にある点を (x’,y’) の位置に移動する変換を 線形変換 といい下記で表せる。

     \begin{eqnarray*} \begin{pmatrix} x' \\ y' \\ \end{pmatrix} = \begin{pmatrix} a & b \\ c & d \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ \end{pmatrix}\end{eqnarray*}

具体的には以下のような変換がある。線形変換では入力画像の直線が直線として出力される性質がある。

  • 拡大縮小: x, y方向に拡大率によって拡大・縮小する変換
  • 回転: 原点を中心に角度θだけ動かす変換
  • せん断: 長方形を傾けて平行四辺形にする変換。または スキュー
  • 鏡映変換: ある直線に対して対称な位置に反転する変換

任意の線形変換の組み合わせは線形変換となることから, 合成可能であり変換行列の積で表すことができる。

平行移動は線形変換では表すことはできず以下となる。tx, ty は平行移動量。

     \begin{eqnarray*} \begin{pmatrix} x' \\ y' \\ 1 \\ \end{pmatrix} = \begin{pmatrix} 1 & 0 & tx \\ 0 & 1 & ty \\ 0 & 0 & 1 \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \\ \end{pmatrix}\end{eqnarray*}

線形変換と同次座標 [2] による平行移動を組み合わせた変換を アフィン変換 といい, cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]]) で行える。

In [15]: height, width = img_src.shape[:2]

In [16]: size = tuple(np.array([width, height]))

In [17]: rad = np.pi/6

In [18]: move_x = width*0.1

In [19]: move_y = height*-0.3

In [20]: afn_mat = np.float32([[np.cos(rad), -1*np.sin(rad), move_x], [np.sin(rad), np
    ...: .cos(rad), move_y]])

In [21]: img_dst = cv2.warpAffine(img_src, afn_mat, size, flags=cv2.INTER_LINEAR)

In [22]: cv2.imshow('Affine Transformation', img_dst)

In [23]: cv2.waitKey(0)

回転角度を rad, 平行移動量を move_x, move_y, また補間手法は INTER_LINEAR (双一次補間, デフォルト値) として変換してみた。

cv2-affine

補間手法は他にも INTER_NEAREST (最近傍補間), INTER_AREA (ピクセル領域の関係を利用したリサンプリング) , バイリニア補間 (周囲4点で補間), バイキュービック補間 (周囲16点で補間) などがある。

濃淡変換

多くの階調で表現された画像を濃淡画像という。例えば二値画像の場合は階調は2, 2bit量子化した場合は階調は4の濃淡画像となる。

ヒストグラム

画像全体の濃淡, チャンネルの特徴を掴むために ヒストグラム (x=画素値, y=度数)を確認してみる。

ヒストグラムを表現するための度数分布は cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]]) で求める。

In [4]: img_gry = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)

In [5]: img_hist = np.zeros([100, 256]).astype("uint8")

In [6]: rows, cols = img_hist.shape

In [7]: hdims = [256]

In [8]: hranges = [0, 256]

In [9]: hist = cv2.calcHist([img_gry], [0], None, hdims, hranges)

In [10]: min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(hist)

In [11]: for i in range(0, 255):
    ...:     v = hist[i]
    ...:     cv2.line(img_hist, (i, rows), (i, rows - rows * (v / max_val)), (255, 255, 255))

In [12]: cv2.imshow('Histogram', img_hist)

In [13]: cv2.waitKey(0)

グレースケール画像のヒストグラムは以下のような分布となった。

cv2-hist-of-org

濃淡変換

濃淡変換として, ヒストグラム均一化やトーンカーブを用いた濃淡変換などがある。

ヒストグラム均一化は各画素値を全体に渡って均一になるように変換する処理。
グレースケール画像のヒストグラム均一化は cv2.equalizeHist(src[, dst]) でできる。

In [14]: img_dst = cv2.equalizeHist(img_gry)

In [15]: cv2.imshow('Equalize Histogram', img_dst)

In [16]: cv2.waitKey(0)

cv2-equalize-hist

銀塩写真のフィルムに映された写真のように明度を反転させる処理として ネガポジ変換 がある。ネガポジ変換のトーンカーブは, 各画素値を反転させるだけなので 255 – 各画素値 で計算できる

In [17]: img_dst = 255 - img_gry

In [18]: cv2.imshow('NP', img_dst)

In [19]: cv2.waitKey(0)

cv2-np

ヒストグラムが反転していることを確認する。

cv2-hist-of-np

明度調整は画像全体を均一に明るく, または暗くする処理。
明度調整のトーンカーブをルックアップテーブル (Look-Up Table, LUT) として定義し, これを使って明度調整する。
具体的には cv2.LUT(src, lut[, dst]) に LUT を指定して変換する。

In [20]: def craete_table(shift):
    ...:     t = np.arange(256, dtype=np.uint8)
    ...:     for i in xrange(0, 255):
    ...:         j = i + shift
    ...:         if j < 0:
    ...:             t[i] = 0
    ...:         elif j > 255:
    ...:             t[i] = 255
    ...:         else:
    ...:             t[i] = j
    ...:     return t
    ...:

In [21]: table = craete_table(shift=100)

In [22]: img_dst = cv2.LUT(img_gry, table)

In [23]: cv2.imshow('Value Adjustment', img_dst)

In [24]: cv2.waitKey(0)

コントラストは画像の濃淡の分布幅に関する性質で, コントラストが高い場合, 濃淡の幅が広く, コントラストが低い場合, 濃淡の幅が狭い。

正規化関数 cv2.normalize() でコントラストを低減してみる。NORM_MINMAXでは alpha に下限, beta に上限値を指定することでその幅に収めるように正規化される。
norm_type には NORM_MINMAX 以外にも, NORM_INFや NORM_L1, NORM_L2が指定できる。

In [25]: cv2.normalize(img_gry, img_dst, alpha=100, beta=200, norm_type=cv2.NORM_MINMAX)
Out[25]:
array([[167, 167, 166, ..., 173, 168, 151],
       [167, 167, 167, ..., 170, 165, 149],
       [167, 167, 167, ..., 175, 169, 155],
       ...,
       [109, 110, 111, ..., 137, 135, 135],
       [108, 110, 112, ..., 139, 137, 138],
       [107, 109, 112, ..., 140, 140, 141]], dtype=uint8)

In [26]: cv2.imshow('Contrast Reducing', img_dst)

In [27]: cv2.waitKey(0)

cv2-contrast-reducing

濃淡の幅が狭くなっていることを確認。

cv2-hist-of-cr

先程とは異なる LUT を定義しコントラスト強調を行う。

In [28]: def craete_enhancement_table(min, max):
    ...:     t = np.arange(256, dtype=np.uint8)
    ...:     for i in xrange(0, min):
    ...:         t[i] = 0
    ...:     for i in xrange(min, max):
    ...:         t[i] = 255 * (i - min) / (max - min)
    ...:     for i in xrange(max, 255):
    ...:         t[i] = 255
    ...:     return t
    ...:

In [29]: table = craete_enhancement_table(min=150, max=200)

In [30]: img_dst = cv2.LUT(img_gry, table)

In [31]: cv2.imshow('Contrast Enhancement', img_dst)

In [32]: cv2.waitKey(0)

cv2-contrast-enhancement

濃淡がはっきり分かれていることを確認。

おわりに

今回は OpenCV3で画像ファイルを読み込み, 読み込んだ画像データに対して基本的な画像処理である 色空間変換, 幾何学変換, 濃淡変換 を試してみました。
Code は GitHub に置きました。次回は, ノイズ除去やエッジ検出などのフィルタ処理について書きます。


[1] PNG, JPEG, TIFF, GIFなど代表的なフォーマット以外にも OpenCV 3.0 から WebP の入出力もサポート
[2] 2×2行列の変換行列で表せられる線形変換では平行移動, 透視変換は実現できないので要素を追加する