画像処理
OpenCV
numpy
行列
daisan

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

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

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

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

1. 画像データ is 何

画像データは行列(多次元配列)です。

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

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 で行列を作って遊んでみます。

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: フィルタによる畳み込み

# 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: ガウシアンフィルタ

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

## 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 行列を計算してそれを判別関数に入れてコーナーがあるかどうか判別します。内部のアルゴリズムは省略。

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)など

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