#はじめに
PythonでOpneCV
を使い始めて間もないのですが
いろいろ試してつまずいたところなどを
つらつら書いていこうと思います.
掻い摘んで話すと
LUT
ってすごいんだな!
って思ったので
LUT
使ってみました!
っていう内容です.
(結果的に)自作の色調変換関数との性能比較も行いました.
※様々なトーンカーブを扱った記事はこちら>>
#動作環境
端末:Windows 10
コンソール:cmd(コマンドプロンプト)
python:3.6.8
仮想環境:venv
cmdを快適にしたい方はこちらへ>>
#導入
私は大学の授業(ペーパーワーク)でComputer Vision(CV)について学びました.
そこではじめて「トーンカーブ」というものに出会いました.
トーンカーブとは1
ディジタル画像の各画素は, その濃淡を表す値(画素値)を持っている. 画像の濃淡を変化させるためには, 入力画像の画素値に対し, 出力画像の画素値をどのように対応づけるかを指定すればよい. そのような対応関係を与える関数のことを諧調変換関数(gray-level transformarion function), また, それをグラフで表したものをトーンカーブ(tone curve)とよぶ.
ネットをみると, カメラ好きの方たちにはおなじみのものみたいです.
教科書に出てきたトーンカーブの中で, 以下のような二つがありました.
(※もちろんほかにもたくさん出てきました)
横軸が入力画素値, 縦軸が出力(変換後の)画素値となります.
図1 | 図2 |
簡単に言うと
コントラストを上げる変換と
コントラストを下げる変換です.
この部分の授業なんて半年前に, とっくに終わっているのですが
このトーンカーブを実際に画像へ適用してみたくなりました.
#関数化
関数で表すと以下のようになります( xは入力画素値 0 ~ 255).
図1の関数:
f(x) = \begin{cases} 2 \cdot x & (x < 128) \\ 255 & (otherwise)
\end{cases}
図2の関数:
f(x) = \begin{cases} 0 & (x < 128) \\ 2 \cdot x - 255 & (otherwise)
\end{cases}
一般化できそうですが, そのことはひとまず置いておきましょう.
#失敗1
まずは, 図1のコードを書こうと思いました.
が...早速詰まりました.
私は, 上の関数を思いついた時点で
単純に元の画素値の配列の要素値を二倍にすればいいと思いました.
def toneCurve(frame):
return 2 * frame
frame = cv2.imread('.\\image\\castle.jpg')
cv2.imwrite('cas.jpg', toneCurve(frame))
frame
は画素値の格納されている配列
しかしうまくいきませんでした.
出力した画像は以下のようになりました.
元画像 | 変換画像 |
---|---|
どうやらcv2.imread()
で取得した画素値を単純に二倍すると
255(最大値)より大きなものは255になる補完されるのではなく
サイクル的に値を変換して返されてしまうようでした.
(ex:256=0, 260=4)
とても禍々しくてこれはこれで好きですが
目的はこの画像ではないので改良します.
#改良1(失敗2)
調べてみると, cv2.add()
を使えば
255以上の値を255として扱ってくれるみたいです.
試してみました.
##図1の改良
自分に自身の画素値を足しこむ = 定数倍
ということになるので
変換関数は以下のようになりました.
def toneCurve11(frame, n = 1):
tmp = frame
if n > 1:
for i in range(1,n):
tmp = cv2.add(tmp, frame)
return tmp
frame
は画素値の格納されている配列
n
は画素値を何倍, つまりaddする回数を表します.
それでは結果です.
元画像 | 変換画像 |
---|---|
そう! 求めていたのはこれです!
コントラストを上げた画像を出力してくれました.
(グレイスケールの画像の方がよかったですか?)
しかし, この足していくだけの方法でどうやって図2の変換を実現し
画像を出力するのか?
パっとひかって思いつきました
##図2の改良
何を思いついたかというと, こういうことです.
ネガポジ反転
> cv2.add()
> ネガポジ反転
図に表すとこうなります.
| | |
|:-:|:-:|:-:|:-:|
|1.無変換|2.ネガポジ反転|
| | |
|3.cv2.add()で倍化|4.ネガポジ反転|
変化は1, 2, 3, 4の順です.
変換用コードは以下のようになりました.
def negaPosi(frame):
return 255 - frame
def toneCurve21(frame, n = 1):
if n > 1:
return negaPoso(toneCurve11(negaPosi(frame), n))
return frame
先ほど同様に
frame
は画素値の格納されている配列
n
は画素値を何倍, つまりaddする回数を表します.
それでは, 結果です.
元画像 | 変換画像 |
---|---|
無事コントラストを下げた画像を出力してくれました.
#小休止
以上のようにやっても, まあうまくいくんですけど
オーダーについて問われそうな気がし, ほかの方法を調べることにしました.
すると, cv2には**LUT(Look Up Table)
**なる便利なものがあるようです.
しかも, オーダーもかなり小さくできると...
#LUT
上でいくつかのアプローチをしてきたわけですが, 結局のところ
取得した画素値を別の画素値に置き換えれば, 変換できるわけですね.
ここで, 画素値の取りうる値は 0 ~ 255 の高々256通りです.
ならば, ある画素値に対する変換先の値をあらかじめ登録した対応表を持っておき
画素値を参照したとき, 画素値自体を計算するのではなく
単純に置き換えるだけにすれば計算コストを抑えることができる.
これがLUTの簡単な説明です.
例えば以下のような対応表が考えられます.
参照画素 | 変更先 | |
---|---|---|
0 | -> | 0 |
1 | -> | 2 |
... | ... | |
127 | -> | 254 |
128 | -> | 255 |
... | ... | |
255 | -> | 255 |
そうです, この表は図1のトーンカーブの対応表です.
もし, frame[90][90] の画素値が
[12, 245, 98]
だったとすると
LUT適用後は
[24, 255, 196]
になります.
#改良2
LUTを実際に用いていきます.
##図1の改良(Re:)
def toneCurve12(frame, n = 1):
look_up_table = np.zeros((256,1), dtype = 'uint8')
for i in range(256):
if i < 256 / n:
look_up_table[i][0] = i * n
else:
look_up_table[i][0] = 255
return cv2.LUT(frame, look_up_table)
frame
は画素値の格納されている配列
n
は画素値を何倍にするかを表しています.
##関数の説明
look_up_table = np.zeros((256,1), dtype = 'uint8')
の部分ですが, 変換用の対応表(256 × 1)を作成します.
現段階ではまだ以下のような値が格納されています.
>>> look_up_table = np.zeros((256,1), dtype = 'uint8')
>>> look_up_table
array([[0],
[0],
[0],
...,
[0],
[0]], dtype=uint8)
>>> len(look_up_table)
256
次の行からこの配列に参照する値を格納していきます.
for i in range(256):
if i < 256 / n:
look_up_table[i][0] = i * n
else:
look_up_table[i][0] = 255
の部分ですが, 先ほど作成した変換用の対応表に, 画素値を登録していきます.
また
if i < 256 / n:
の部分は条件式を一般化し, 画素値がn倍であっても動作するようになっています.
(ex: n=3[画素値3倍])
return cv2.LUT(frame, look_up_table)
の部分ですが, 先ほど登録したLUTによって対応関係をとり,
変換された画像データを返します.
それでは, 結果です.
元画像 | 改良1 | 改良2 |
---|---|---|
目的の画像を得ることができています. |
実行時間(ms)についてみてみましょう.
n = 2 | n = 3 | n = 4 | |
---|---|---|---|
改良1 | 4.9848556 | 9.9725723 | 15.921831 |
改良2 | 4.9870014 | 4.9834251 | 4.9870014 |
処理が速くなりました. |
同様にして, 図2を改良していきます.
##図2の改良(Re:)
def toneCurve22(frame, n = 1):
look_up_table = np.zeros((256,1), dtype = 'uint8')
for i in range(256):
if i < 256 - 256 / n :
look_up_table[i][0] = 0
else:
look_up_table[i][0] = i * n - 255 * (n - 1)
return cv2.LUT(frame, look_up_table)
やっていることはほぼ変わらないので, 詳しい説明は省きますが,
if i < 256 - 256 / n :
の部分は, n = 2
の時の関数部分で x < 128
だったものを一般化したものです.
n = 2とすると
x < 256 - 256 / n = 128
つまり,
x < 128
に変形できます.
それでは, 結果です.
元画像 | 改良1 | 改良2 |
---|---|---|
目的の画像を得ることができています. |
実行時間(ms)についてみてみましょう.
n = 2 | n = 3 | n = 4 | |
---|---|---|---|
改良1 | 25.915145 | 32.911539 | 36.406755 |
改良2 | 4.9862861 | 4.9872398 | 4.9846172 |
やっぱり, 処理の早いものがいいですね.
#おわりに
今回は, トーンカーブについてだらだらと話してきました.
OpenCVってすごく便利ですね.
先人達には頭が上がりません...
しかも, Pythonならコマンド一つで使えるようになります.
この記事だれか見てくれるのだろうか?
とか, 色々不安になりながらも
これは備忘録だと, 保険をかけて
いつも投稿しています.
こんな記事もいつか誰かの役に立つといいですね.
さいごに今回用いたソースコードを張って終了とします.
次は何をしようかな?
それでは
#ソースコード
カレントディレクトリ下の
imageというディレクトリ内に
castle.jpgという画像ファイルが存在します.
このコードを使用される場合は, 各自変更してください.
また, 以下でライブラリをインストールしてください.
$ pip install opencv-python
$ pip freeze
numpy==1.18.4
opencv-python==4.2.0.34
import cv2
import numpy as np
def toneCurve(frame):
return 2 * frame
def negaPosi(frame):
return 255 - frame
def toneCurve11(frame, n = 1):
tmp = frame
if n > 1:
for i in range(1, n):
tmp = cv2.add(tmp, frame)
return tmp
def toneCurve12(frame, n = 1):
look_up_table = np.zeros((256, 1), dtype = 'uint8')
for i in range(256):
if i < 256 / n:
look_up_table[i][0] = i * n
else:
look_up_table[i][0] = 255
return cv2.LUT(frame, look_up_table)
def toneCurve21(frame, n = 1):
if n > 1:
return negaPosi(toneCurve11(negaPosi(frame), n))
return frame
def toneCurve22(frame, n = 1):
look_up_table = np.zeros((256, 1), dtype = 'uint8')
for i in range(256):
if i < 256 - 256 / n :
look_up_table[i][0] = 0
else:
look_up_table[i][0] = i * n - 255 * (n - 1)
return cv2.LUT(frame, look_up_table)
def main():
img_path = '.\\image\\castle.jpg'
img = cv2.imread(img_path)
cv2.imwrite('.\\image\\tone00.jpg', toneCurve(img))
cv2.imwrite('.\\image\\tone11.jpg', toneCurve11(img, 2))
cv2.imwrite('.\\image\\tone12.jpg', toneCurve12(img, 2))
cv2.imwrite('.\\image\\tone21.jpg', toneCurve21(img, 2))
cv2.imwrite('.\\image\\tone22.jpg', toneCurve22(img, 2))
if __name__ == '__main__':
main()
#参考
- コンピュータ グラフィックス
- Optie研 (はてなブログ)
- Pythonの学習の過程とか (はてなブログ)
- 画像取得元 (Pixabay)
#関連記事
-
コンピュータ グラフィックス(p262)より抜粋 ↩