LoginSignup
8
6

More than 3 years have passed since last update.

画像処理のトーンカーブについての話しをしよう ~LUTはすごいぞ~

Last updated at Posted at 2020-06-03

はじめに

PythonでOpneCVを使い始めて間もないのですが
いろいろ試してつまずいたところなどを
つらつら書いていこうと思います.

掻い摘んで話すと
LUTってすごいんだな!
って思ったので
LUT使ってみました!
っていう内容です.

(結果的に)自作の色調変換関数との性能比較も行いました.

※様々なトーンカーブを扱った記事はこちら>>

動作環境

端末:Windows 10
コンソール:cmd(コマンドプロンプト)
python:3.6.8
仮想環境:venv

cmdを快適にしたい方はこちらへ>>

導入

私は大学の授業(ペーパーワーク)でComputer Vision(CV)について学びました.
そこではじめて「トーンカーブ」というものに出会いました.
トーンカーブとは1

ディジタル画像の各画素は, その濃淡を表す値(画素値)を持っている. 画像の濃淡を変化させるためには, 入力画像の画素値に対し, 出力画像の画素値をどのように対応づけるかを指定すればよい. そのような対応関係を与える関数のことを諧調変換関数(gray-level transformarion function), また, それをグラフで表したものをトーンカーブ(tone curve)とよぶ.

ネットをみると, カメラ好きの方たちにはおなじみのものみたいです.

教科書に出てきたトーンカーブの中で, 以下のような二つがありました.
(※もちろんほかにもたくさん出てきました)

横軸が入力画素値, 縦軸が出力(変換後の)画素値となります.

toneCurve12.png toneCurve22.png
図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は画素値の格納されている配列

しかしうまくいきませんでした.
出力した画像は以下のようになりました.

元画像 変換画像
元画像 失敗画像1

どうやら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の変換を実現し
画像を出力するのか?

パっとひかって思いつきました:fireworks:

図2の改良

何を思いついたかというと, こういうことです.

ネガポジ反転 > cv2.add() > ネガポジ反転

図に表すとこうなります.

ori negapoei
1.無変換 2.ネガポジ反転
add renegaposi
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する回数を表します.

それでは, 結果です.

元画像 変換画像
元画像 失敗画像3

無事コントラストを下げた画像を出力してくれました.

小休止

以上のようにやっても, まあうまくいくんですけど
オーダーについて問われそうな気がし, ほかの方法を調べることにしました.

すると, 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
元画像 失敗画像2 成功画像1

目的の画像を得ることができています.

実行時間(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
元画像 失敗画像2 成功画像1

目的の画像を得ることができています.

実行時間(ms)についてみてみましょう.

n = 2 n = 3 n = 4
改良1 25.915145 32.911539 36.406755
改良2 4.9862861 4.9872398 4.9846172

やっぱり, 処理の早いものがいいですね.

おわりに

今回は, トーンカーブについてだらだらと話してきました.

OpenCVってすごく便利ですね.
先人達には頭が上がりません...
しかも, Pythonならコマンド一つで使えるようになります.

この記事だれか見てくれるのだろうか?
とか, 色々不安になりながらも
これは備忘録だと, 保険をかけて
いつも投稿しています.

こんな記事もいつか誰かの役に立つといいですね.

さいごに今回用いたソースコードを張って終了とします.
次は何をしようかな?

それでは:wave:

ソースコード

カレントディレクトリ下の
imageというディレクトリ内に
castle.jpgという画像ファイルが存在します.
このコードを使用される場合は, 各自変更してください.

また, 以下でライブラリをインストールしてください.

コンソール
$ pip install opencv-python

$ pip freeze
numpy==1.18.4
opencv-python==4.2.0.34
ex.py
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()

参考

関連記事

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6