画像処理
1. 目的
プログラミングによってディジタル画像処理の基礎的な技術を理解する。
2. 理論
2.1 OpenCV
OpenCVとは、画像処理・画像の機械学習などのためのオープンソースライブラリである。C++やMATLABなどでも使えるが、今回はPythonで使用する。ライブラリを用いることで、様々な画像処理の演算を簡単に実装できる。
2.2 画像の基礎知識
ディジタル画像と画素
ディジタル画像は、画素と呼ばれる濃淡を表す小さな点が格子状に並ぶことによって構成される。画素のイメージを図1に示す。各画素の値は画素値(pixel value)とよばれ、濃度を整数値で表現している。例えば、画素値を1 bit で表現すれば0, 1の白黒画像、8 bitで表現すれば256階調(0~255)のグレースケール画像となる。
光の三原色(three primary colors of light)とカラー画像
カラー画像を扱う場合は、画素値に赤(R)/緑(G)/青(B)の色を表す値を利用することがある。このRGBの3色は光の三原色とよばれ、図2のようにこれらの色を混ぜ合わせることによってさまざまな色を作り出すことができる。例えば、ある画素を、RGBそれぞれ8bitずつ、全24bitで表現すれば、16,777,216色(256×256×256階調)を表示することができる。
ヒストグラム(histogram)
ヒストグラムは、数学では度数分布表を指す。画像処理の分野では、横軸を画素値として、各画素値の出現回数をカウントしたグラフを指す。画像の統計的な情報を知るための方法として利用される。
2.3 画像の幾何変換
画像のxy座標をベクトルと考えると、それぞれのベクトルに行列をかけることで座標を変換することができる。たとえば の座標をそのまま に移す行列は以下の通りである。
拡大縮小
以下の行列で拡大縮小を行うことができる。 は 方向の拡大率、 は 方向の拡大率を示す。
回転
x軸のプラス方向が右で、y軸のプラス方向が上のとき、原点を中心に、反時計回りに 回転させるには、以下の行列を用いる。
ただし、片方の軸のプラス方向が異なる場合、回転方向が逆転する。たとえば、x軸のプラス方向が右で、y軸のプラス方向が下のとき、上の行列では時計回りに回転する。
これらの処理を重ねて行う場合、行列のかけ算を行うことで重ねた効果を得られる。
これらやスキューに加え、平行移動をまとめて扱うためのアフィン変換がある。アフィン変換では以下のような行列を使う。
2.4 線形フィルタ
線形フィルタでは、処理の対象としている画素(注目画素)の値だけでなく、その周辺の画素値も参照して演算を行う。図3にフィルタの一例を示す。注目画素を中心とした3x3の領域において、各画素について画素値×フィルタ値の計算を行い、その和を注目画素の新たな画素値とする。
図3は平滑化フィルタを示す。周囲の画素を平均化することで、画素値の細かな変化を小さくしている。
図4は鮮鋭化フィルタを示す。平滑化フィルタとは逆に、周囲の画素との差を際立たせる効果がある。
図5は横の、図6は縦の微分フィルタを示す。1つ横または縦の隣と画素との差分をとることで、画素値の大きく変化している部分、すなわち輪郭(エッジ)を抽出することができる。
(ここから先の理論は、レポートに記述する必要はない。)
2.5 リファレンス
プログラミングで知らない関数や機能の使い方を調べる時、最も正確な情報を示すのがリファレンスマニュアルである。
リファレンスは世界中の人が内容を正しく理解できるように、多くの場合英語で記述してある。いくつかの例を示す。
リファレンスは一見難解な書き方で分かりにくいが、機能がどういった挙動をしているのか正確に知るためにはリファレンスを参照するのが最良の方法である。
たとえば、cv2.warpAffine()
関数の使い方を確かめる。この関数は、画像にアフィン変換を適用する。
https://docs.opencv.org/4.5.3/da/d54/group__imgproc__transform.html
リファレンスを見ると、Pythonでの使用方法は以下のように記述してある。
cv.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]]) -> dst
[]
でくくってある引数はオプションのためなくてもよい。これを省くと以下のようになる。
cv.warpAffine(src, M, dsize) -> dst
->
は関数の戻り値を表しているため、実際のPythonプログラムは以下のように記述できる。
dst = cv2.warpAffine(src, M, dsize)
それぞれのパラメータについても説明がある。
- src: 入力画像
- dst: 出力画像
- M: 変換に使う2行3列の行列
- dsize: 出力画像のサイズ
これを参考にプログラムを作成する。
たとえば、入力と出力が同じである変換を行うプログラムは、
# ライブラリの読み込み
import os
from IPython.display import Image, display
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 画像の読み込み
drive_path = '/content/drive/MyDrive'
lena_gray = cv2.imread(os.path.join(drive_path, "lena_gray.png"))
# 画像は画素値の配列として保存されている
# 画像の高さ(height), 幅(width), チャンネル数(channels)を取得する
h, w, c = lena_gray.shape
# 変換に使う行列。異なる変換を行う場合、この行列を変更する
mat = np.float32([[1, 0, 0],
[0, 1, 0]])
# 変換を行う関数
# dst = cv2.warpAffine(src, M, dsize)
lena_translated = cv2.warpAffine(lena_gray, mat, (w, h))
# 画像の表示
show_lena = cv2.imencode('.png', lena)[1]
display(Image(show_lena))
と記述できる。
Note
(この部分はレポートに書く必要はありません)
このプログラムは、Googleドライブを接続するプログラムを実行後に動作する。
3. 実験方法
以下のリンクから、Jupyter Notebookのテンプレートを使用できる。
https://colab.research.google.com/github/ohashi-gnct/exp/blob/2023/image.ipynb
3.1 実験準備
まずは、各種ライブラリをインポートする。Colabの環境に最初からインストールされていないライブラリは、インストールしてからインポートする。
以下のプログラムを順に実行する。
import os
from IPython.display import Image, display
import cv2
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
!pip install japanize-matplotlib
# matplotlibの日本語化
import japanize_matplotlib
sns.set(font="IPAexGothic")
# 軸ラベル等の文字サイズを大きくする
sns.set_context('talk')
今回の実験では、使用する画像lena.png
や実験結果の画像を自身のGoogleドライブに保存する。
# Googleドライブの接続
from google.colab import drive
drive.mount('/content/drive', force_remount=True)
drive_path = '/content/drive/MyDrive'
画像をUNIXコマンドwget
でダウンロードし、自身のGoogleドライブに保存する。
%cd $drive_path
!wget https://www.gifu-nct.ac.jp/elec/ohashi/python/documents/docs/images/image/lena.png
Googleドライブにlena.png
が存在することを確認してもよい。
以下のプログラムで、画像を表示する。
# 画像を読み込む
lena = cv2.imread(os.path.join(drive_path, "lena.png"))
# 画像を表示する
show_lena = cv2.imencode('.png', lena)[1]
display(Image(show_lena))
グラフの形式で表示される。
画像表示のプログラムは後の実験で何度も使用するため、一連のプログラムを関数(function)としてまとめておく。
def imshow(img):
show_img = cv2.imencode('.png', img)[1]
display(Image(show_img))
Note
(この部分は実験方法に書く必要はありません)
def
の次の行からは、関数の中身であることを示すため行頭に同じ数のスペースであるインデントを入れている。上の例では2つスペースを入れている。
このように関数を定義することで、
inshow(lena)
関数を呼び出して画像を表示できる。
Note
(この部分は実験方法に書く必要はありません)
時間を置いて実行したり、ウェブページを閉じて再度開いて途中から実行すると、import
関連のエラーが出る。その場合、ランタイムが切断され実行状態が保存されていないため、最初のインポートから実行する。
3.2 画像の情報取得
lena
の[50, 50]と[150, 150]の画素値を取得する。
# 画素値を取得するコードは自分で考える
Tip
(この部分は実験方法に書く必要はありません)
配列のように座標を指定してprint()
することで、画素値を表示できる。画素値は配列の配列である2次元配列に格納されている。
そのため、lena
に対して、[縦の座標, 横の座標]
という形で配列のインデックスを指定する。なお座標の原点は左上である。
また、画像の縦横それぞれの画素数もプログラムで取得する。画素数はlena.shape
が保持している。
h, w, c = lena.shape
print(h, w, c)
今回の実験ではグレースケールの画像を使うため、読み込んだ画像をグレースケール化する。
lena_gray = cv2.cvtColor(lena, cv2.COLOR_BGR2GRAY)
imshow(lena_gray)
プログラムでlena_gray
の[50, 50]と[150, 150]の画素値を取得する。
# 画素値を取得するコードは自分で考える
グレースケール画像を保存する。
cv2.imwrite(os.path.join(drive_path, "lena_gray.png"), lena_gray)
Googleドライブから、新しい画像ファイルを確認する。
次に、グレースケール画像のヒストグラムを取得する。保存したグレースケール画像を使用し、以下のようなプログラムを作成する。
lena_hist = cv2.calcHist([lena_gray], [0], None, [256], [0, 256])
# 軸ラベルをつけるなど、グラフの体裁を整えるプログラムを自分で考える
plt.plot(lena_hist)
plt.show()
Tip
(この部分は実験方法に書く必要はありません)
このグラフで、横軸は画素値(高いほうが明るい)、縦軸はその画素値を持つ画素の数を表している。
ここまでで行う実験は以下の通りである。
lena.png
の[50, 50]
と[150, 150]
の画素値を取得する。- 画像をグレースケール化し、
lena_gray.png
として保存する。 lena_gray.png
を読み込み、[50, 50]
と[150, 150]
の画素値を取得する。lena_gray.png
のヒストグラムを保存する。
3.3 画像の幾何変換
画像変換のための行列は次のように指定できる。
行列が2行3列となっているが、一番右の列は両方0
を入れる。たとえば、入力の座標がそのまま出力となる行列は以下の通りである。
mat = np.float32([[1, 0, 0],
[0, 1, 0]])
以下のようにプログラムに組み込むことで、幾何変換を行った画像を表示できる。
mat = np.float32([[1, 0, 0],
[0, 1, 0]])
h, w = lena_gray.shape
lena_translated = cv2.warpAffine(lena_gray, mat, (w, h))
imshow(lena_translated)
- 入力をy(縦)方向に2倍する変換を適用する。
- 入力を反時計回りに45度回転させる変換を適用する。
- 入力をy方向に0.5倍してから、反時計回りに30度回転させる変換を適用する。
- 入力を反時計回りに30度回転させてから、y方向を0.5倍にする変換を適用する。
Note
(この部分は実験方法に書く必要はありません)
変換後の画像がはみ出たり余白が追加されるかもしれませんが、そのままで構いません。直したい場合は直してください。
Tip
(この部分は実験方法に書く必要はありません)
複数の変換を行うには、変換を行った画像を入力としてさらに変換を行う。または、先に行列の積を計算し、その行列を変換に使用する。
3.4 線形フィルタ
画像にフィルタを適用するために、フィルタの配列を作る必要がある。フィルタの配列を作成する。
Tip
(この部分は実験方法に書く必要はありません)
前の実験で幾何変換のために2行3列の配列(行列)を作成した。今回は3行3列の行列が必要となる。
フィルタを適用するには、cv2.filter2D()
関数を使う。
cv2.filter2D()
関数の使い方は以下のリファレンスを参考にする。
https://docs.opencv.org/4.5.3/d4/d86/group__imgproc__filter.html
Tip
(この部分は実験方法に書く必要はありません)
リファレンスを見ると、cv2.filter2D()
は以下のように使うと書いてある。
cv.filter2D(src, ddepth, kernel) -> dst
->
は関数の戻り値を表しているため、実際のプログラムでは以下のように使える。
dst = cv2.filter2D(src, ddepth, kernel)
引数ddepth
は、出力画像のビット深度を指定している。今回は入力と同じことを示す-1
を指定する。
以下のフィルタをかけたlena_gray.png
をそれぞれ保存する。
- 平滑化フィルタ
- 鮮鋭化フィルタ
- 横の微分フィルタ
課題
すべての実験結果を一覧で見られるようにし、先生にチェックをもらう。
行列による変換のデモ
下の行列の値を変更から「行列で変換」すると、任意の変換を行えます。
線形フィルタのデモ