Image Processing Collection in Python

Pythonでよく使う画像処理ライブラリの関数まとめ備忘録。
随時更新予定。

以下のライブラリをimportしていることが前提
## Library Version
opencv @3.2.0_1+contrib+python27
scikit-image @0.12.3_2
scipy @0.19.0_0+gfortran

```py
import numpy as np
import copy
import cv2 as cv
import skimage
import skimage.io as sk
import skimage.filters as fil
import skimage.filters.rank as rank
import skimage.morphology as mor
import skimage.transform as tr
import scipy.misc as sp
import scipy.ndimage as ndi
```

# Read Image
画像の読み込みに使うライブラリごとの比較

### scikit-image

```python
img = sk.imread(filename,as_grey=False,plugin=None,flatten=None)
```

* マルチチャネル画像読み込み可能　(3D multi channel 画像にはこれ)
* as_gray=TrueでRGB画像を読み込む際にグレイスケール変換してくれるが、64bit-floatになって画素値が潰されてしまうため気を付ける必要あり

### OpenCV

```py
img = cv.imread((filename[, flags])
```

* flags
  + cv.IMREAD_COLOR (or 1) : RGB画像を読み込み
  + cv.IMREAD_GRAYSCALE（or 0）: グレイスケールに変換して読み込み
  + cv.IMREAD_UNCHANGED（or -1）: 変換なし読み込み (α チャネルも読み込み可能)
* RGB画像の読み込みはチャネルがBGRになるから注意
* cv.cvtColor(image, cv2.COLOR_BGR2RGB)でRGBに変換可能

### SciPy

```py
img = sp.imread(name, flatten=False, mode=None)
```
* 普段あまり使わないが、上の二つで無理だった場合頼る
* mode : str, optional
  * 'L' : グレイスケール変換読み込み(8-bit)
  * 'RGB' : RGB画像読み込み (3x8-bit)
  * 'RGBA' : 4 channel読み込み(4x8-bit)
  * 'I' : グレイスケール変換読み込み (32-bit signed integer)
  * 'F' : グレイスケール変換読み込み (32-bit floating point)
  * 他にも 'P', 'CMYK', 'YCbCr' がある


# Binalization

画像の二値化は基本的に大津くらいしか用いないが、目的によっては他の手法も用いるので参考までに。

### scikit-image

```py
# 各二値化手法の閾値を求める
thrOtsu = fil.threshold_otsu(img)
thrli = fil.threshold_li(img)
thrIso = fil.threshold_isodata(img)
thrYen = fil.threshold_yen(img)

# 二値化
thrImg = ((thrOtsu < img) * 255).astype(np.uint8)
```

![binalization.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/137574/88838ee1-2bc1-f3ad-0099-8d8782baf0e2.jpeg)

### OpenCV

まったく使わないが読み込みなど一貫してOpenCVを使ってるときなどに。

```py
thr, thrImg = cv.threshold(img, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
```


# Filtering

主に画像処理の前処理として行うノイズ除去に用いる。
ノイズ除去では、ノイズの種類や画像の特徴に合わせて様々な filtering を使い分ける必要がある。
なのでここでの構成は、使い分けできるように filtering の種類ごとにまとめる。

### Gaussian Fitering
ガウス分布に従った重みカーネルで畳み込む。一般的な画像の平滑化に用いる。

```python
# 正規分布の分散
sigma = 1
# カーネルサイズ
ksize = (3, 3)

# OpenCV
gauImg = cv.GaussianBlur(img, ksize=ksize, sigmaX=sigma)

# SciPy
gauImg = ndi.gaussian_filter(img, sigma=sigma)

# scikit-image
# imgがfloat型の場合は-1~1に正規化する必要あり
gauImg = fil.gaussian_filter(img, sigma=sigma)

```

### Mean Filtering

平均重みカーネルで畳み込む。
一様ノイズのような細かいノイズなどを除去する場合に用いる。

```python
# カーネルサイズ
ksize = (5, 5)
# 半径20pixel円の重みカーネル
selem = disk(20)

# OpenCV
meanImg = cv.blur(img, ksize=ksize)

# scikit-image
# imgがfloat型の場合は-1~1に正規化する必要あり
meanImg = rank.mean(img, selem=selem)
```

### Median Filtering

中央値フィルタリング。
ゴマ塩ノイズのようなスパイク状のノイズなどを除去する場合に用いる。

```python
# カーネルサイズ
ksize = 3
# 半径20pixel円の重みカーネル
selem = disk(20)

# OpenCV
medImg = cv.medianBlur(img, ksize=ksize)

# scikit-learn
# imgがfloat型の場合は-1~1に正規化する必要あり
medImg = rank.median(img, selem=selem)
```

### Bilateral Filtering

Gaussian filtering ではエッジなど特徴的な画素も含めて平滑化してしまうことから、その後の画像処理によっては問題となってくる。
そこでBilateral filtering では、エッジを保持しつつ平滑化を行うことができる。
Gaussian filteringとの決定的な違いは、カーネル中心からの距離差に関するガウス分布だけでなく、中心輝度値からの輝度差分に関するガウス分布に従った重みカーネル

```python
# カーネルサイズ
d = 5
# 輝度値ガウス分布の分散
sigmaColor = 1
# 距離差ガウス分布の分散
sigmaSpace = 1

# OpenCV
biImage = cv.bilateralFilter(img, d=d, sigmaColor=sigmaColor, sigmaSpace=sigmaSpace)
```

# Morphological Operation

主にグレースケールや二値画像のノイズ・バックグラウンド除去、特徴抽出など、その後の画像処理に大きく影響する処理を行う。
この節でも各モルフォロジ演算処理ごとにまとめる。
個人的には、DilationとErosionだけライブラリから拝借してopeningやclosing関数を構築した方が色々と自由度増していいと思っていまふ。

### Dilation

二値画像のオブジェクトを膨張させる処理。

```python
# カーネルサイズ
kernel = np.ones((3,3),np.uint8)
# 試行回数
iterations = 2

# OpenCV
dilImg = cv.dilate(thrImg, kernel=kernel, iterations=iterations)

# scikit-image
dilImg = mor.dilation(thrImg, selem=kernel)
```

### Erosion

二値画像のオブジェクトを収縮させる処理。

```python
# OpenCV
eroImg = cv.erode(thrImg, kernel=kernel, iterations=iterations)

# scikit-image
eroImg = mor.erosion(thrImg, selem=kernel)
```

### Opening

小さなノイズなどを除去したいときなどに用いる。
複数回のErosion後に同じ回数だけDilationを行う。このとき各処理回数は除去したいノイズの大きさに依存する。

```python
# OpenCV
openImg = cv.morphologyEx(thrImg, cv.MORPH_OPEN, kernel=kernel, iterations=iterations)

# scikit-image
openImg = mor.opening(thrImg, selem=kernel)
```

### Closing

ノイズの影響や画像の性質上、オブジェクトが綺麗に二値化できなかったときに生じる穴などをfillするときに用いる。
複数回のDilation後に同じ回数だけErosionを行う。このとき各処理回数はfillしたい穴の大きさなどに依存する。

```python
# OpenCV
closeImg = cv.morphologyEx(thrImg, cv.MORPH_CLOSE, kernel=kernel, iterations=iterations)

# scikit-image
closeImg = mor.closing(thrImg)
```

### White Top-Hat

opening処理した画像と元画像の差分を出すことで、バックグラウンドに存在した小さい特徴パターンを抽出する際に用いる。

```python
# OpenCV
wthImg = cv.morphologyEx(thrImg, cv.MORPH_TOPHAT, kernel=kernel, iterations=iterations)

# scikit-image
wthImg = mor.white_tophat(thrImg)
```

### Black Top-Hat

closing処理した画像と元画像の差分を出すことで、オブジェクト内に存在した小さい特徴パターンを抽出する際に用いる。

```python
# OpenCV
bthImg = cv.morphologyEx(thrImg, cv.MORPH_BLACKHAT, kernel=kernel, iterations=iterations)

# scikit-image
bthImg = mor.black_tophat(thrImg)
```

![morphorogy2.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/137574/4f02548d-487a-fc62-5cdd-7309aeee5382.jpeg)


# Labeling

binary画像におけるセグメント領域をそれぞれラベリングする処理。
主に二値化後の定量的解析 (面積や重心座標などを求める) を行う際に用いる。
二次元画像のみの対応だが、OpenCVに便利な関数が用意されていたので使ってみた。

```python
# ラベル数(nlabel)とラベリング画像(labImg)のみ
nlabel, labImg = cv.connectedComponents(img)

# 上に加えてバウンディングボックス+面積(stats)と重心座標(centroids)も取得
nlabel, labImg, stats, centroids = cv.connectedComponentsWithStats(img)
```

stats : 各ラベルに対応して [x, y, width, height, area] をリストで格納
centroid : 同じくラベルに対応して重心座標 [x, y] をリストで格納

実際に以下の画像に対して使ってみた結果を示す。
![unko_img.002.png](https://qiita-image-store.s3.amazonaws.com/0/137574/4b6c7168-f809-c024-4b0a-25e24a28fd7f.png)

```python
img = sk.imread('unko.png')
gimg = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
thrOtsu = fil.threshold_otsu(gimg)
thrImg = ((thrOtsu > img) * 255).astype(np.uint8)
nlabel, labelImg, stats, centroids = cv.connectedComponentsWithStats(thrImg)
colors = []
for n in range(1, nlabel):
   # 各うんこをcrop
   tmpImg = img[stats[n][1]:stats[n][1]+stats[n][3], stats[n][0]:stats[n][0]+stats[n][2]] sk.imsave('unko' + str(n) + '.png', tmpImg)
   color.append(np.array([random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)]))
leny, lenx = labelImg.shape
colorImg = np.zeros((leny, lenx, 3)).astype(np.uint8)
for x in range(lenx):
   for y in range(leny):
      for n in range(1, nlabel):
         # 各うんこ領域のラベルに従い色付け
         if labelImg[y][x] == n:
            colorImg[y][x] = color[n-1]
sk.imsave('unko_color.png', colorImg)
```
ラベリング関数から得られたstatsから各うんこをcropし保存した結果

![unko_img.003.png](https://qiita-image-store.s3.amazonaws.com/0/137574/0350201c-c839-dc76-a2ec-969d13f12464.png)

各うんこ領域をカラーラベリングした結果

![unko_img.004.png](https://qiita-image-store.s3.amazonaws.com/0/137574/3a3f2b54-f3e4-4168-66cd-4424b4e0b675.png)

これは便利！うんこまみれ！


# Rotation of Image

画像の回転を行う関数まとめ。
今回は画像の中心から回転させる場合についてのみまとめる。

```python
# 回転角度(反時計回り)
angle = 90

# Scipy
rotImg = ndi.interpolation.rotate(img, angle=angle)

# OpenCV
imsize = img.shape
rotMat = cv.getRotationMatrix2D(tuple(np.array(imsize)/2), angle, scale=1.0)
rotImg = cv.warpAffine(img, rotMat, imsize)
```

また、画像の反転(flip)を行う関数もある。

```python
flipCode = 0

# OpenCV
flipImg = cv.flip(img, flipCode=flipCode)
```

引数であるflipCodeは反転方向を制御するパラメータ

* 0 : x軸反転
* 正 : y軸反転
* 負 : x, y両軸反転 (180度rotationと同じ)



# Interpolation

画像の補間を行う関数まとめ。
画像補間手法として、Nearest, Bilinear, Bicubicなどがあり、画像や目的に応じて使い分ける。

```python
# 補間サイズ
ipsize = (200, 200)

# OpneCV
# 補間法
cv.INTER_NEAREST  # Nearest
cv.INTER_LINEAR   # Bilinear
cv.INTER_CUBIC    # Bicubic
ipImg = cv.resize(img, ipsize, metLinear)

# scikit-image
# 補間法 (Biquinticなども用意されている)
order = 0  # Nearest
order = 1  # Bilinear (default)
order = 3  # Bicubic
# imgがfloatの場合preserve_rangeをTrueにしておく必要あり
ipImg = tr.resize(img, ipsize, order = 3, preserve_range = True)
```

![interpolation.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/137574/87536b23-cb1b-73b0-ae5f-578caefdce34.jpeg)


# Extract Feature

画像におけるエッジなどの特徴を抽出する。
特徴量抽出アルゴリズムであるSIFTの他にも、微分フィルタによるエッジ抽出もこの節にまとめる。

### SIFT

SIFT (Scale-Invariant Feature Transform) は、特徴量抽出アルゴリズムの中で最も用いられているものの一つで、スケールや位置変化にロバストな特徴量を抽出してくれる。
主に画像認識の分野でも、パターン認識や機械学習などにおいて用いられる。

OpenCVに用意されている関数は、3.0からcontribモジュールに格納されているため、インストールする際にvariantに追加しておく必要がある。
また、2.4までのバージョンで行う方法も別にあるが、今回は3.0以降のOpenCVであることを前提にまとめる。

```python
# 特徴量抽出
det = cv.xfeatures2d.SIFT_create()
kp = det.detect(img)   # imgはCV_8U(uint8)

# 画像に付加
siftImg = cv.drawKeypoints(img, kp, None)
```

### SURF

SURF (Speeded-Up Robust Features) は、SIFTのアルゴリズムにおける特徴量を表すベクトルの次元数を削減する代わりに、処理速度を上げたアルゴリズム。

```python
# 特徴量抽出
det = cv.xfeatures2d.SURF_create()
kp = det.detect(img)   # imgはCV_8U(uint8)

# 画像に付加
surfImg = cv.drawKeypoints(img, kp, None)
```

![siftandsurf.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/137574/8c547e28-df78-8556-997c-22767df09c3f.jpeg)


### Prewitt Filtering

画像内の輝度勾配を検出するためのフィルタリング手法。
Prewitt Filterのコンセプトは、カーネルにおけるある方向の勾配を計算する際に、その方向と水平方向に平滑化を行ってから微分を行うというもの。

```python
# SciPy
preImg = ndi.filters.prewitt(img)
```

### Sobel Filtering

Prewitt Filterとほとんど同じ微分フィルタであるが、さらにカーネル内の中央に重み付けしてある点が異なる。
個人的にはSobel Filterの方がエッジをきれいに取れてこれる印象。

```python
# SciPy
sobImg = ndi.filters.sobel(img)
```

![presob.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/137574/672f9a63-0899-8e91-6399-b2fc79794503.jpeg)


# Others

### Downhill Filtering

バックグラウンド(BG)除去に用いるアルゴリズム。中身は元論文[1]参照。

```python
# seed (BGとしてみなす値の基準) の作成
seed = copy.deepcopy(img)
seed[1:-1, 1:-1] = img.min()

dilatedImg = mor.reconstruction(seed, img, method='dilation')
dhImg = copy.deepcopy(img - dilated)
```

![downhill.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/137574/c8a95105-9c9c-80d4-dac0-5ac6c3c9d0c6.jpeg)


# まとめ

画像処理においてどのライブラリを使うにしても、アルゴリズムを理解しないで用いることは危険なため、中身を熟知している使い慣れているライブラリで一貫してCordingしていくことが大事だと思う。
また、自分が行う画像解析では主にグレースケール画像が対象であるため、RGB画像における画像処理は苦手で今回の内容にもあまり反映できていない。
今後はRGB画像も見据えたマルチチャネル画像処理を習得していきたい。
内容は随時更新していく予定。

# 参考文献
[1] Robinson, Kevin, and Paul F. Whelan. "Efficient morphological reconstruction: a downhill filter." Pattern Recognition Letters 25.15 (2004): 1759-1767.