python で画像処理と行列超入門

？？？「Structure from Motion するために2つの画像の特徴点を取って対応点を見つけて〜」

特徴点？　対応点？　何を仰るのだこの御仁は。

同僚に突然こんなふうに話しかけられたときに戸惑わないために、画像処理の超基本を書いておきます。 OpenCV python を使います。

## 1. 画像データ is 何

画像データは行列（多次元配列）です。

以下の画像（320x180ピクセル）を読み込んで、サイズを表示してみます。OpenCV で読み込んが画像データは numpy array になります。

```py
import cv2

# 画像を読み込んで形を表示
image = cv2.imread("image.png")
print(image.shape)
# -> (180, 320, 3)
# (縦, 横, 色)という3次元配列

# グレースケールに変換
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print(gray_image.shape)
# -> (180, 320)
# グレースケールは1色なので2次元配列になる
```

## 2. 行列 is 何

行列はある演算規則の定まった2次元配列です（一般的に多次元になるとテンソルといいます）。数学のさまざまな分野で出てきます。

### 行列で遊ぼう1：　基本演算

numpy で行列を作って遊んでみます。

```py
import numpy as np

a1 = np.array([
    [1, 2],
    [3, 4]
])

a2 = np.array([
    [5, 6],
    [7, 8]
])

# 行列の足し算は、要素同士の足し算
a1 + a2
# [[ 6  8]
#  [10 12]]

# 行列の引き算は、要素同士の引き算
a1 - a2
# [[-4 -4]
#  [-4 -4]]

# 行列のかけ算は、ちょっと複雑（演算規則は省略）
np.dot(a1, a2)
# [[19 22]
#  [43 50]]

# "1" っぽい役割のもの
# 「単位行列」といいます
e = np.array([
    [1, 0],
    [0, 1]
])
# 任意の行列とかけ算すると…
np.array_equal(
    np.dot(e, a1),
    a1
)
# -> True
np.array_equal(
    np.dot(a1, e),
    a1
)
# -> True
# "1 * x = x * 1 = x" みたいなのが成り立つ！

# わり算は？
# 逆数が定義できればいいよね
# 逆数とは：　"x * x^(-1) = x^(-1) * x = 1" が成り立つとき x^(-1) が x の逆数
a1_inverse = np.linalg.inv(a1)
# 行列 a1 とその逆行列の積が単位行列になる！
np.dot(a1_inverse, a1).astype(np.int32) # 見やすさのため整数型に変換
# [[1 0]
#  [0 1]]
np.dot(a1, a1_inverse).astype(np.int32)
# [[1 0]
#  [0 1]]

# わり算（の代わりに逆数をかける）
np.dot(a2, a1_inverse)
```

### 行列で遊ぼう2：　フィルタによる畳み込み

```py
# 5x5 のグレースケール画像の行列
image = np.array([
    [  0,  0,  0,  0,  0],
    [  0,  0,  0,  0,  0],
    [  0,  0,255,255,  0],
    [  0,  0,255,255,  0],
    [  0,  0,  0,  0,  0],
]).astype(np.float32)

# フィルタによる畳込み

# 何もしないカーネル
id_kernel = np.array([
    [0, 0, 0],
    [0, 1, 0],
    [0, 0, 0],
]).astype(np.float32)
# 元の image と変わらない
cv2.filter2D(image, -1, id_kernel).astype(np.int32)
# [[  0   0   0   0   0]
#  [  0   0   0   0   0]
#  [  0   0 255 255   0]
#  [  0   0 255 255   0]
#  [  0   0   0   0   0]]

# 1ピクセル右にずらすカーネル
right_kernel = np.array([
    [0, 0, 0],
    [1, 0, 0],
    [0, 0, 0],
]).astype(np.float32)
cv2.filter2D(image, -1 ,right_kernel).astype(np.int32)
# [[  0   0   0   0   0]
#  [  0   0   0   0   0]
#  [  0   0   0 255 255]
#  [  0   0   0 255 255]
#  [  0   0   0   0   0]]

# 画像の微分は実はフィルタによる畳み込み
d_x = np.array([
    [-1, 0, 1],
    [-1, 0, 1],
    [-1, 0, 1],
]).astype(np.float32)
d_y = np.array([
    [-1,-1,-1],
    [ 0, 0, 0],
    [ 1, 1, 1]
]).astype(np.float32)
cv2.filter2D(image, -1, d_x).astype(np.int32)
# [[   0    0    0    0    0]
#  [   0  255  255 -255    0]
#  [   0  510  510 -510    0]
#  [   0  510  510 -510    0]
#  [   0  510  510 -510    0]]
cv2.filter2D(image, -1, d_y).astype(np.int32)
# [[   0    0    0    0    0]
#  [   0  255  510  510  510]
#  [   0  255  510  510  510]
#  [   0 -255 -510 -510 -510]
#  [   0    0    0    0    0]]
```

### 行列で遊ぼう3：　ガウシアンフィルタ

ガウシアンフィルタで画像の「ぼかし」ができます。

```py
## 5x5のグレースケール画像
image = np.array([
    [  0,  0,  0,  0,  0],
    [  0,  0,  0,  0,  0],
    [  0,  0,100,100,  0],
    [  0,  0,100,100,  0],
    [  0,  0,  0,  0,  0],
]).astype(np.float32)

# ガウシアンフィルタ
gaussian_kernel = np.array([
  [1,  2, 1],
  [2,  4, 2],
  [1,  2, 1]
]).astype(np.float32) / 16
gauss_image = cv2.filter2D(image, -1 , gaussian_kernel).astype(np.int32)
print(gauss_image)
# [[ 0  0  0  0  0]
#  [ 0  6 18 18 12]
#  [ 0 18 56 56 37]
#  [ 0 18 56 56 37]
#  [ 0 12 37 37 25]]
```

## 特徴点 is 何

+ コーナーなど画像内の特徴的な点
+ 物体追跡や対応点検出で使う

### Harris コーナー検出器

特徴点検出のやり方としては、画像の各点ごとに Harris 行列を計算してそれを判別関数に入れてコーナーがあるかどうか判別します。内部のアルゴリズムは省略。

```py
import numpy as np
import cv2

# 5x5 のグレースケール画像
image = np.array([
    [  0,  0,  0,  0,  0,  0,  0,  0,  0,  0],
    [  0,  0,  0,  0,  0,  0,  0,  0,  0,  0],
    [  0,  0,  0,  0,  0,255,255,255,255,255],
    [  0,  0,  0,  0,  0,255,255,255,255,255],
    [  0,  0,  0,  0,  0,255,255,255,255,255],
]).astype(np.float32)

# コーナー検出
dst = cv2.cornerHarris(image,2,3,0.04)
# 二値化処理
threshold = (dst > 0.01 * dst.max()).astype(np.int32)
print(threshold)
# [[0 0 0 0 0 0 0 0 0 0]
#  [0 0 0 0 0 0 0 0 0 0]
#  [0 0 0 0 0 1 1 0 0 0]
#  [0 0 0 0 0 1 1 0 0 0]
#  [0 0 0 0 0 0 0 0 0 0]]
```

### 特徴量記述子

特徴点だけでは座標しかわからないので、対応点検出のためにはピクセルの周辺情報を含んだ「特徴記述子」が必要となる。

+ ある点の周辺の画像情報を次元を落としたベクトルにする
  * SIFTなら128次元のベクトル
+ 記述子はスケール、回転、明度などに対してロバスト（変更に強い性質）でなければならない
+ ある記述子と別の記述子が「近い」といえるために距離を定義する
+ 特徴量記述子のマッチングを効率よく行うアルゴリズムを作る
  * 近似最近傍探索(FLANN based matcher)など

くたびれたからこれでおしまい。