はじめに
2018/11/30-12/1に、ソフトピアジャパンの研修「オープンソースによる画像処理・認識プログラム開発~OpenCV編~」を受講しました。授業としては簡単な環境構築→テストプログラムを実装、という形で実施、言語はC++。自分としてはPythonで実装もしたかったので、授業内容の振り返りも兼ねてPythonで構築しつつ、ソースコードの内容を振り返ります。
本記事でやりたいこと
・OpenCVの環境構築ができたので、さくっと自分のプログラムを動かしつつ、どんな処理をしているのかをソースコードと併せて説明したい
本記事で割愛すること
・OpenCVの環境構築
使用した書籍
OpenCVによる画像処理入門 改訂第2版 (KS情報科学専門書)
講義もそうですが、上記の教科書が良かった。基本的な画像処理の理論面での復習はこれで良さそう。
ただし、3言語「対応」なだけで、「網羅」ではないところは注意。
とはいいつつも、完全網羅するとページ数が爆発しちゃいますので、これくらいが許容かなぁと。
私について
10年前の卒業研究でC++とOpenCVで福祉×画像処理の研究をやっていたが、それ依頼画像処理はほとんど実施せず、久しぶりに勉強してみるか・・という状態。悪戦苦闘の度合いは講義時のTwitterモーメントが分かりやすいかと。
本記事での環境構築
本記事でのソースコードは以下で構成されています。ディストリビュータ(Anaconda)の説明は割愛します。(環境構築割愛のため)
・Anaconda Navigator:1.9.4
・Python: 3.7.1
・OpenCV: 3.4.2
実装
以下の記事に関する説明
基本構成
本記事は以下のセットで構成します。
・動作内容:どのような動作をするか
・動作写真:動作結果の写真(入力画像には基本的にLennaを使っています)
・ソースコード:コード直張り・GitHub
基本用語定義
画処理系で基本的に使用されている言葉です。以下の用語は今後説明せず、適宜使用します。
・src:入力画像/動画 (source)
・dst:出力画像/動画 (destination)
1.画像反転(OpenCVのはじめの一歩のはじめの一歩として)
動作内容
src→垂直反転→dst
動作写真

import cv2
file_src = 'src.png'
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst', cv2.WINDOW_NORMAL)
img_src = cv2.imread(file_src, 1)
img_dst = cv2.flip(img_src, flipCode = 0)
cv2.imshow('src', img_src)
cv2.imshow('dst', img_dst)
cv2.destroyAllWindows()
2.色空間変換・グレースケール変換
動作内容
src→色空間変換(RGB → HSV)
src→グレースケール変換(RGB → GRAY) → dst
動作写真

ソースコード
import cv2
file_src = 'src.png'
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_hsv', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_gray', cv2.WINDOW_NORMAL)
img_src = cv2.imread(file_src, 1)
img_dst_hsv = cv2.cvtColor(img_src, cv2.COLOR_BGR2HSV)
img_dst_gray = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)
cv2.imshow('src', img_src)
cv2.imshow('dst_hsv', img_dst_hsv)
cv2.imshow('dst_gray', img_dst_gray)
cv2.waitKey(0)
cv2.destroyAllWindows()
3.画像のヒストグラム表示
動作内容
src→グレースケール変換→グレースケール画素数分布図(ヒストグラム)作成→dst
※そもそも「ヒストグラム」とは、横軸に度数、縦軸に分布数を配置した図の総称である(度数分布図)。
画像処理関係での「ヒストグラム」は以下の用途で使われる。(何を横軸にしているのか注意)
・輝度
・画素
・明度
上記のことが理解できていれば、下記の表記もなんとなくわかるはず。
「明度を上下させる」→「ヒストグラムの分布を左右に動かす」
「コントラストを上下させる」→「ヒストグラムの表示幅を広狭させる」
なお、OpenCVでのグレースケール変換(cvtColor:BGR2GRAY)では、以下の変換式が使用されており(公式ドキュメントより)、
RGB[A] to Gray:Y←0.299⋅R+0.587⋅G+0.114⋅B
ヒストグラムの算出も以下の結果の値を参照されている。
本記事では例として、画素ヒストグラム均一化※させた画像との比較を掲載する。
※画素ヒストグラム均一化:ヒストグラムの分布に偏りがあるものを画像全体に均一化させたもの。
(画素の累積度数をグラフ化すると、1次関数の形になる)
グレースケールの画像だと、くっきり見えるようになる場合が多い。
動作写真
ソースコード
import cv2
import numpy as np
file_src = 'src.png'
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('gray_original', cv2.WINDOW_NORMAL)
cv2.namedWindow('gray_equalize', cv2.WINDOW_NORMAL)
cv2.namedWindow('hst_original', cv2.WINDOW_NORMAL)
cv2.namedWindow('hst_equalize', cv2.WINDOW_NORMAL)
img_src = cv2.imread(file_src, 1)
img_gray = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)
img_gray_equalize = cv2.equalizeHist(img_gray)
img_hst_original = np.zeros([100, 256]).astype('uint8')
img_hst_equalize = np.zeros([100, 256]).astype('uint8')
rows, cols = img_hst_original.shape[:2]
hdims = [256]
hranges = [0, 256]
hist_original = cv2.calcHist([img_gray], [0], None, hdims, hranges)
hist_equalize = cv2.calcHist([img_gray_equalize], [0], None, hdims, hranges)
min_val_original, max_val_original, min_loc_original, max_loc_original = cv2.minMaxLoc(
hist_original)
min_val_equalize, max_val_equalize, min_loc_equalize, max_loc_equalize = cv2.minMaxLoc(
hist_equalize)
for i in range(0, 255):
v = hist_original[i]
cv2.line(img_hst_original, (i, rows), (i, rows -
rows * (v / max_val_original)), (255, 255, 255))
s = hist_equalize[i]
cv2.line(img_hst_equalize, (i, rows), (i, rows -
rows * (s / max_val_equalize)), (255, 255, 255))
cv2.imshow('src', img_src)
cv2.imshow('gray_original', img_gray)
cv2.imshow('gray_equalize', img_gray_equalize)
cv2.imshow('hst_original', img_hst_original)
cv2.imshow('hst_equalize', img_hst_equalize)
cv2.waitKey(0)
cv2.destroyAllWindows()
4.トーンカーブ
動作内容
src→ガンマ変換→dst
画像全体の明るさや色を補正したいときに使用するときに使われる手法。
入力の画素/階調、縦軸に出力の画素/階調が対応しており、階調変換関数と呼ばれている。
階調変換関数の2次元グラフをルックアップテーブルと呼ぶ。
ルックアップテーブルの作り方によって折れ線変換、ガンマ変換、ネガポジ変換など、様々な変換手法に分かれる。
今回はルックアップテーブルの例として、画像全体の明るさを変えることができるガンマ変換(γ=1.5)を例にして紹介する。
動作写真

ソースコード
import cv2
import numpy as np
LUT_GAMMA = 1.5
file_src = 'src.png'
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst', cv2.WINDOW_NORMAL)
img_src = cv2.imread(file_src, 1)
lkup_tbl = np.zeros((256, 1), dtype='uint8')
for i in range(256):
lkup_tbl[i][0] = 255 * pow(float(i) / 255, 1.0 / LUT_GAMMA)
img_dst = cv2.LUT(img_src, lkup_tbl)
cv2.imshow('src', img_src)
cv2.imshow('dst', img_dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
5.ディザリング(擬似濃淡)
動作内容
src→グレースケール化→単純2値化→dst
→ディザリング2値化→dst
グレースケールされた画像を単純2値化(一定の閾値による変換)させると、画像が潰れる傾向がある。
そこで、2値画像には別のアルゴリズムを、つまり2値でも「擬似的に濃淡が表現できるように見える」表現方法を用いることが多い。
その手法がディザリング(擬似濃淡処理)である。
画素値の情報を落としても、(グレースケールさせても)、濃淡を表現できるので、データ量を落とす方法と考えることができる。
ただし、擬似的な手法であるため、想定通りの濃淡変換が出なかったり、元の画像にはない濃淡が発生する場合もある。
動作例として、単純2値化画像(左上)、ランダムディザリング(左下)、誤差拡散ディザリング(右上)、組織的ディザリング(Bayer,右下)された画像の比較結果を紹介する。
※参考書籍では、Bayerディザ行列のMatrixのレンジ拡張のアルゴリズムに誤りがあるので、4*4以外に拡張する場合は要注意
動作写真

ソースコード
import cv2
import numpy as np
import random
LUT_GAMMA = 1.5
file_src = 'src.png'
cv2.namedWindow('gray', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_2', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_random', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_errdiffusion', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_bayer', cv2.WINDOW_NORMAL)
img_src = cv2.imread(file_src, 1)
img_gray = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)
# 二値変換
THRESH = 100
MAX_PIXEL = 255
_, img_dst_2 = cv2.threshold(img_gray,THRESH,MAX_PIXEL,cv2.THRESH_BINARY)
# ディザリング
SIZE_IMAGE = 512, 512, 1
img_dst_random = np.zeros(SIZE_IMAGE, dtype=np.uint8)
img_dst_errdiffusion = np.zeros(SIZE_IMAGE, dtype=np.uint8)
img_dst_bayer = np.zeros(SIZE_IMAGE, dtype=np.uint8)
# ランダムディザリング
for y in range(SIZE_IMAGE[0]):
for x in range(SIZE_IMAGE[1]):
rand = random.random()*MAX_PIXEL
if img_gray[y][x] > rand:
img_dst_random[y][x] = MAX_PIXEL
else:
img_dst_random[y][x] = 0
# 誤差拡散ディザリング
THRESH_ERRDIFFUSION = 128
errval_errfissufion = 0
for y in range(SIZE_IMAGE[0]):
for x in range(SIZE_IMAGE[1]):
if (img_gray[y][x] + errval_errfissufion) < THRESH_ERRDIFFUSION:
img_dst_errdiffusion[y][x] = 0
errval_errfissufion = img_gray[y][x]
else:
img_dst_errdiffusion[y][x] = MAX_PIXEL
errval_errfissufion = img_gray[y][x] - MAX_PIXEL
# 組織的ディザリング(Bayer)
N_BAYER = 4
matrix_bayer = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]]
magnification_bayer = (MAX_PIXEL+1)/(N_BAYER*N_BAYER)
for x in range(N_BAYER):
for y in range(N_BAYER):
matrix_bayer[y][x] *= magnification_bayer
for y in range(SIZE_IMAGE[0]):
for x in range(SIZE_IMAGE[1]):
if img_gray[y][x] < matrix_bayer[y % N_BAYER][x % N_BAYER]:
img_dst_bayer[y][x] = 0
else:
img_dst_bayer[y][x] = MAX_PIXEL
# 表示
cv2.imshow('dst_2', img_dst_2)
cv2.imshow('dst_random', img_dst_random)
cv2.imshow('dst_errdiffusion', img_dst_errdiffusion)
cv2.imshow('dst_bayer', img_dst_bayer)
cv2.waitKey(0)
cv2.destroyAllWindows()
フィルタ処理
動作内容
src→フィルタ処理→dst
単一の画素に対する画素変換ではなく、対象の画素およびその周辺領域の画素値を利用して出力画素値を決定するような処理。フィルタには種類があり、平均/加重平均(平滑化)、ガウシアン(ピンぼけ)、中央(ノイズ除去)、Sobel・ラプラシアン(エッジ検出)など。
動作例として、加重平均オペレータ(右上)、ガウシアンフィルタ(左下)、ラプラシアンフィルタ(右下)を紹介する。
動作写真

ソースコード
import cv2
file_src = 'src.png'
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_weightedave', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_gaussian', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst_laplacian', cv2.WINDOW_NORMAL)
img_src = cv2.imread(file_src, 1)
# フィルタ処理
img_dst_weightedave = cv2.blur(img_src, (3, 3))
img_dst_gaussian = cv2.GaussianBlur(img_src, (11, 11), 1)
img_dst_laplacian = cv2.Laplacian(img_src, cv2.CV_32F, 3)
cv2.imshow('src', img_src)
cv2.imshow('dst_weightedave', img_dst_weightedave)
cv2.imshow('dst_gaussian', img_dst_gaussian)
cv2.imshow('dst_laplacian', img_dst_laplacian)
cv2.waitKey(0)
cv2.destroyAllWindows()
おわりに
前半部分だけでもなかなかの時間を要したので、前後編にします。
後半は2値画像のマスク・形状特徴抽出、画像の幾何学変換、デモアプリの開発などを記載します。