1. はじめに
画像の前処理の技術力向上のためにこちらを実践 画像処理100本ノック!!
とっかかりやすいようにColaboratoryでやります。
目標は2週間で完了できるようにやっていきます。丁寧に解説します。質問バシバシください!
001 - 010 は右のリンク 画像処理100本ノック!!(001 - 010)丁寧にじっくりと
011 - 020 は右のリンク 画像処理100本ノック!!(011 - 020)序盤戦
2. 前準備
ライブラリ等々を以下のように導入。
# ライブラリをインポート
from google.colab import drive
import numpy as np
import matplotlib.pyplot as plt
import cv2
from google.colab.patches import cv2_imshow
# 画像の読み込み
img = cv2.imread('画像のパス/imori.jpg')
img_noise = cv2.imread('画像のパス/imori_noise.jpg')
img_dark = cv2.imread('画像のパス/imori_dark.jpg')
img_gamma = cv2.imread('画像のパス/imori_gamma.jpg')
# グレースケール画像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray_noise = cv2.cvtColor(img_noise, cv2.COLOR_BGR2GRAY)
gray_dark = cv2.cvtColor(img_dark, cv2.COLOR_BGR2GRAY)
# 画像保存用
OUT_DIR = '出力先のパス/OUTPUT/'
3.解説
Q.21. ヒストグラム正規化
ヒストグラム正規化を実装せよ。
ヒストグラムは偏りを持っていることが伺える。 例えば、0に近い画素が多ければ画像は全体的に暗く、255に近い画素が多ければ画像は明るくなる。 ヒストグラムが局所的に偏っていることをダイナミックレンジが狭いなどと表現する。 そのため画像を人の目に見やすくするために、ヒストグラムを正規化したり平坦化したりなどの処理が必要である。
このヒストグラム正規化は濃度階調変換(gray-scale transformation) と呼ばれ、[c,d]の画素値を持つ画像を[a,b]のレンジに変換する場合は次式で実現できる。 今回はimori_dark.jpgを[0, 255]のレンジにそれぞれ変換する。
def hist_normalization(img, a=0, b=255):
"""
ヒストグラム正規化
params
----------------------------
param1: numpy.ndarray形式のimage
param2: ヒストグラムレンジの最小値
param3: ヒストグラムレンジの最大値
returns
----------------------------
numpy.ndarray形式のimage
"""
# ヒストグラム(rgb)の最大値・最小値の値
c = img.min() # 60
d = img.max() # 141
# コピー
out = img.copy()
# 正規化
out = (b - a) / (d - c) * (out - c) + a
out[out < a] = a
out[out > b] = b
out = out.astype(np.uint8)
return out
# 画像の高さ、幅、色を取得
H, W, C = img_dark.shape
# ヒストグラム正規化
out = hist_normalization(img_dark)
# ヒストグラムを表示する
plt.hist(out.ravel(), bins=255, rwidth=0.8, range=(0, 255))
plt.savefig("img21.png")
plt.show()
Q.22. ヒストグラム操作
ヒストグラムの平均値をm0=128、標準偏差をs0=52になるように操作せよ。
これはヒストグラムのダイナミックレンジを変更するのではなく、ヒストグラムを平坦に変更する操作である。
平均値m、標準偏差s、のヒストグラムを平均値m0, 標準偏差s0に変更するには、次式によって変換する。
def hist_mani(img, m0=128, s0=52):
"""
ヒストグラムの平均値をm0=128、標準偏差をs0=52になるように操作
params
--------------------------------------
param1: numpy.ndarray形式のimage
param2: 平均値
param3: 標準偏差
returns
--------------------------------------
numpy.ndarray形式のimage
"""
# 平均値
m = np.mean(img)
# 標準偏差
s = np.std(img)
# 画像のコピー
out = img.copy()
# 計算式通りに計算
out = s0 / s * (out - m) + m0
out[out < 0] = 0
out[out > 255] = 255
out = out.astype(np.uint8)
return out
# ヒストグラムを操作
out = hist_mani(img_dark)
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans22_1.jpg', out)
# 画像を表示
cv2_imshow(out)
cv2.waitKey(0)
cv2.destroyAllWindows()
# ヒストグラムを表示する
plt.hist(out.ravel(), bins=255, rwidth=0.8, range=(0, 255))
plt.savefig("img22_2.png")
plt.show()
Q.23. ヒストグラム平坦化
ヒストグラム平坦化を実装せよ。
ヒストグラム平坦化とはヒストグラムを平坦に変更する操作であり、上記の平均値や標準偏差などを必要とせず、ヒストグラム値を均衡にする操作である。
これは次式で定義される。 ただし、S ... 画素値の総数、Zmax ... 画素値の最大値、h(z) ... 濃度zの度数
def hist_equal(img, z_max=255):
"""
ヒストグラム平坦化
params
--------------------------------------
param1: numpy.ndarray形式のimage
param2: 画素値の最大値
returns
--------------------------------------
numpy.ndarray形式のimage
"""
# 画像の高さ、幅、色を取得
H, W, C = img.shape
# 画素値の総数(画像高さx画像幅x色数)
S = H * W * C * 1. # 49152.0
# 画像のコピー
out = img.copy()
# 濃度の度数
sum_h = 0.
# 画像の濃度0~255までそれぞれの度数
for i in range(256):
# 濃度が一致する箇所
ind = np.where(img==i)
# 一致した濃度の度数
sum_h += len(img[ind])
# ヒストグラム値を均衡(計算式参照)
z_prime = z_max / S * sum_h
out[ind] = z_prime
out = out.astype(np.uint8)
return out
# ヒストグラムを操作
out = hist_equal(img)
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans23_1.jpg', out)
# 画像を表示
cv2_imshow(out)
cv2.waitKey(0)
cv2.destroyAllWindows()
# ヒストグラムを表示する
plt.hist(out.ravel(), bins=255, rwidth=0.8, range=(0, 255))
plt.savefig("img23_2.png")
plt.show()
Q.24. ガンマ補正
そこで、ガンマ補正は次式で行われる。imori_gamma.jpgに対してガンマ補正(c=1, g=2.2)を実行せよ。
ガンマ補正とは、カメラなどの媒体の経由によって画素値が非線形的に変換された場合の補正である。 ディスプレイなどで画像をそのまま表示すると画面が暗くなってしまうため、RGBの値を予め大きくすることで、ディスプレイの特性を排除した画像表示を行うことがガンマ補正の目的である。
非線形変換は次式で起こるとされる。 ただしxは[0,1]に正規化されている。 c ... 定数、g ... ガンマ特性(通常は2.2)
def gamma_correction(img, c=1, g=2.2):
"""
ガンマ補正・・・画像の明るさを調整する方法
params
--------------------------------------
param1: numpy.ndarray形式のimage
param2: 定数
param3: ガンマ特性
returns
--------------------------------------
numpy.ndarray形式のimage
"""
# 画像のコピー
out = img.copy().astype(np.float)
# 255で割る(Iinに変換)
out /= 255.
# ガンマ補正の式
out = (1/c * out) ** (1/g)
# 255をかける
out *= 255
out = out.astype(np.uint8)
return out
# ガンマ補正
out = gamma_correction(img_gamma)
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans24.jpg', out)
# 画像を表示
cv2_imshow(out)
cv2.waitKey(0)
cv2.destroyAllWindows()
参考: Pythonやってみる!
Q.25. 最近傍補間
最近傍補間により画像を1.5倍に拡大せよ。
最近傍補間(Nearest Neighbor)は画像の拡大時に最近傍にある画素をそのまま使う手法である。 シンプルで処理速度が速いが、画質の劣化は著しい。
次式で補間される。 I' ... 拡大後の画像、 I ... 拡大前の画像、a ... 拡大率、[ ] ... 四捨五入
"""
最近傍補間
cv2.resize(src, dsize[, interpolation])
src 入力画像
dsize 変更後の画像サイズ
interpolation 補間法(最近傍補間ならcv2.INTER_NEAREST)
"""
# 最近傍補間
# 変更後の画像サイズ: img.shape>>>(高さ、幅、色)
out = cv2.resize(
img, (int(img.shape[1]*1.5), int(img.shape[0]*1.5)), interpolation=cv2.INTER_NEAREST)
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans25.jpg', out)
# 画像を表示
cv2_imshow(out)
cv2.waitKey(0)
cv2.destroyAllWindows()
参考: 【Python/OpenCV】画像の拡大・縮小(最近傍補間法、バイリニア補間法、バイキュービック補間法)
Q.26. Bi-linear補間
Bi-linear補間により画像を1.5倍に拡大せよ。
Bi-linear補間とは周辺の4画素に距離に応じた重みをつけることで補完する手法である。 計算量が多いだけ処理時間がかかるが、画質の劣化を抑えることができる。
"""
バイリニア補間法(Bi-linear interpolation)は、周囲の4つの画素を用いた補間法
cv2.resize(src, dsize[, interpolation])
src 入力画像
dsize 変更後の画像サイズ
interpolation 補間法(バイリニア補間ならcv2.INTER_LINEAR)
"""
# バイリニア補間法
# 変更後の画像サイズ: img.shape>>>(高さ、幅、色)
out = cv2.resize(
img, (int(img.shape[1]*1.5), int(img.shape[0]*1.5)), interpolation=cv2.INTER_LINEAR)
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans26.jpg', out)
# 画像を表示
cv2_imshow(out)
cv2.waitKey(0)
cv2.destroyAllWindows()
参考: 【Python/OpenCV】画像の拡大・縮小(最近傍補間法、バイリニア補間法、バイキュービック補間法)
Q.27. Bi-cubic補間
Bi-cubic補間により画像を1.5倍に拡大せよ。
Bi-cubic補間とはBi-linear補間の拡張であり、周辺の16画素から補間を行う。
"""
バイキュービック補間法では、周囲16画素の画素値を利用
cv2.resize(src, dsize[, interpolation])
src 入力画像
dsize 変更後の画像サイズ
interpolation 補間法(バイキュービック補間ならcv2.INTER_CUBIC)
"""
# バイキュービック補間
# 変更後の画像サイズ: img.shape>>>(高さ、幅、色)
out = cv2.resize(
img, (int(img.shape[1]*1.5), int(img.shape[0]*1.5)), interpolation=cv2.INTER_CUBIC)
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans27.jpg', out)
# 画像を表示
cv2_imshow(out)
cv2.waitKey(0)
cv2.destroyAllWindows()
参考: 【Python/OpenCV】画像の拡大・縮小(最近傍補間法、バイリニア補間法、バイキュービック補間法)
Q.28. アフィン変換(平行移動)
アフィン変換を利用して画像をx方向に+30、y方向に-30だけ平行移動させよ。
アフィン変換とは3x3の行列を用いて画像の変換を行う操作である。
"""
アフィン変換
cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])
第一引数に元画像(NumPy配列ndarray)、
第二引数に2 x 3の変換行列(NumPy配列ndarray)、
第三引数に出力画像のサイズ(タプル)を指定する。
"""
# 画像の高さ、幅、色を取得
H, W, C = img.shape
# 平行移動[[1,0,横方向への移動量],[0,1,縦方向への移動量]]の2x3行列
M = np.float64([[1, 0, 30], [0,1,-30]])
# アフィン変換
out = cv2.warpAffine(img, M, (W, H))
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans28.jpg', out)
# 画像を表示
cv2_imshow(out)
cv2.waitKey(0)
cv2.destroyAllWindows()
参考: 【Python/OpenCV】アフィン変換で画像の回転
Q.29. アフィン変換(拡大縮小)
アフィン変換を用いて、(1)x方向に1.3倍、y方向に0.8倍にリサイズせよ。
また、(2) (1)の条件に加えて、x方向に+30、y方向に-30だけ平行移動を同時に実現せよ。
def affine_expand(img, ratio_x, ratio_y):
"""
アフィン変換で拡大
params
-------------------------------
param1: numpy.ndarray形式のimage
param2: x方向の比率
param3: y方向の比率
returns
-------------------------------
numpy.ndarray形式のimage
"""
# 画像の高さ、幅
H, W = img.shape[:2]
# xy座標をnp.float32型
src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
# x, yそれぞれ比率をかける
dest = src.copy()
dest[:,0] *= ratio_x
dest[:,1] *= ratio_y
"""
アフィン変換の変換行列を生成: cv2.getAffineTransform(src, dest)
src: 変換前の3点の座標
dest: 変換後の3点の座標をNumPy配列ndarrayで指定
"""
affine = cv2.getAffineTransform(src, dest)
"""
アフィン変換
cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])
第一引数に元画像(NumPy配列ndarray)、
第二引数に2 x 3の変換行列(NumPy配列ndarray)、
第三引数に出力画像のサイズ(タプル)を指定する。
INTER_LANCZOS4 – 8×8 の近傍領域を利用するLanczos法での補間
"""
return cv2.warpAffine(img, affine, (int(W*ratio_x), int(H*ratio_y)), cv2.INTER_LANCZOS4) # 補間法も指定できる
# アフィン変換で拡大
out = affine_expand(img, 1.3, 0.8)
# 平行移動[[1,0,横方向への移動量],[0,1,縦方向への移動量]]の2x3行列
H, W = out.shape[:2]
M = np.float64([[1, 0, 30], [0,1,-30]])
out2 = cv2.warpAffine(out, M, (W, H))
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans29_1.jpg', out)
cv2.imwrite(OUT_DIR + 'ans29_2.jpg', out2)
# 画像を表示
cv2_imshow(out)
cv2_imshow(out2)
cv2.waitKey(0)
cv2.destroyAllWindows()
参考: 完全に理解するアフィン変換
Q.30. アフィン変換(回転)
(1)アフィン変換を用いて、反時計方向に30度回転させよ。
(2) アフィン変換を用いて、反時計方向に30度回転した画像で中心座標を固定することで、なるべく黒い領域がなくなるように画像を作成せよ。 (ただし、単純なアフィン変換を行うと画像が切れてしまうので、工夫を要する。)
def affin_rotate(img, x, y, theta, scale):
"""
アフィン変換で回転
params
-------------------------------
param1: numpy.ndarray形式のimage
param2: 回転軸のx座標
param3: 回転軸のy座標
param4: 回転角
param5: 回転角度・拡大率
returns
-------------------------------
numpy.ndarray形式のimage
"""
"""
2次元回転を表すアフィン変換
cv2.getRotationMatrix2D(center, angle, scale)
center: 回転の原点となる座標
angle: 回転の角度(ラジアンではなく度degree)
scale: 拡大・縮小倍率。
"""
# 回転変換行列の算出
R = cv2.getRotationMatrix2D((x, y), theta, scale)
"""
アフィン変換
cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])
第一引数に元画像(NumPy配列ndarray)、
第二引数に2 x 3の変換行列(NumPy配列ndarray)、
第三引数に出力画像のサイズ(タプル)を指定する。
cv2.INTER_CUBIC: バイキュービック
"""
# アフィン変換
dst = cv2.warpAffine(img, R, gray.shape,
flags=cv2.INTER_CUBIC)
return dst
# 画像の中心座標
oy, ox = int(img.shape[0]/2), int(img.shape[1]/2)
# 反時計方向に30度回転
out1 = affin_rotate(img, 0, 0, 30, 1)
# 反時計方向に30度回転した画像で中心座標を固定
out2 = affin_rotate(img, ox, oy, 30, 1)
# 結果を保存する
cv2.imwrite(OUT_DIR + 'ans30_1.jpg', out1)
cv2.imwrite(OUT_DIR + 'ans31_2.jpg', out2)
# 画像を表示
cv2_imshow(out1)
cv2_imshow(out2)
cv2.waitKey(0)
cv2.destroyAllWindows()
参考: Python, OpenCVで幾何変換(アフィン変換・射影変換など)
参考: 【Python/OpenCV】アフィン変換で画像の回転
感想
レベルが徐々に上がっているように感じる。可能な限りOpenCVでの実装を心がける。