はじめに
今回から画像処理のプログラムをGLSLでの実装でやっていきます。デモでは素のWebGLを用いていますが、画面全体の板ポリを貼っているだけなのでGLSLのコードだけ解説します。
コードは下記のGitHubのリポジトリのsrc/canvasで公開しています。
色調変換とは
画像処理のなかでももっとも単純な、色調変換を用いたエフェクト処理を紹介します。色調とは色彩の強弱・濃淡など色合いの調子を指します。色調変換の処理は、処理対象となる画像上でひとつの画素値から出力後の画素値を求め、その値に変化させることにより行われます。
数式で書くと、もとの画素の値を、変化後の値をとしたときに次の演算を全画素に対して施します。
ここで、は色調変換などを起こすための関数で、この関数を工夫することでさまざまな効果を得ることができます。
下図のように、もとの画素の値と変化後の値のグラフを考えてみます。の点線の上側をとおるように関数を設定すると、値が大きくなる方向に変化するので全体に画像を明るくすることができます(左図)。逆に下側をとおるように設定すると、全体に画像が暗くなります(右図)。

最初は、明度を調整するような関数をみていきましょう!
明度の調整
上で説明したとおり、色調変換は明度の調整に利用できます。
今回は線形関数とガンマ補正・シグモイド関数、ヒストグラム均等化の処理をGLSLで実装します。
線形関数
線形関数は次のような形になる関数になります。
先ほどの図のように、明るくする関数と暗くする関数を考えてみます。
GLSLで実装するので、uniform変数として0.0 ~ 1.0の範囲でのparam1とparam2を与え、これを用いて線形関数を実装します。
数式としては次のような関数になります。
この関数をグラフで表すと次のようになるでしょう。

式とグラフの通りに、、の場合は元の画像のままになります。また、の値を上げていくと明るい画像になり、の値を下げていくと暗い画像になることが分かるでしょう。
GLSLで書くと次のようになります。
uniform sampler2D uTexture;
uniform float param1;
uniform float param2;
in vec2 vUv;
out vec4 fragColor;
void main() {
vec2 uv = vUv;
vec4 texture = texture(uTexture, uv);
float p1 = param1;
float p2 = param2;
vec3 color = (p2 - p1) * texture.rgb + p1;
fragColor = vec4(color, texture.a);
}結果は次の画像になります。デモではパラメータをいじれるようにしてるので、ぜひ試してみてください!

ガンマ補正
ガンマ補正では、次のような数式を用いて行います。
この関数はグラフで表すと、曲線のトーンカーブになります。

このグラフのように、の値により形状が変化し、のときは上に凸、のときは下に凸のトーンカーブになります。また、の値を上げていくと明るい画像になり、下げていくと暗い画像になります。
をuniform変数として、GLSLで書くと次のようになります。
uniform sampler2D uTexture;
uniform float gamma;
in vec2 vUv;
out vec4 fragColor;
void main() {
vec2 uv = vUv;
vec4 texture = texture(uTexture, uv);
vec3 color = pow(texture.rgb, vec3(1.0 / gamma));
fragColor = vec4(color, texture.a);
}GLSLで累乗を計算するには、pow関数を使用します。
結果は次の画像になります。こちらもデモでパラメータをいじってみてください!

シグモイド関数の利用(S字型トーンカーブ)
続いてはシグモイド関数を利用した例を考えてみましょう。
シグモイド関数は次のような関数になります。
ここではゲイン(gain)と呼びます。このパラメータを変更することでトーンカーブの形状を変更できます。グラフで表すと次のようになります。(のとき)

画像処理で使う場合は、画像値の半分が中間地点になるように式を変形します。

ゲインの値を大きくすれば坂の部分が急になるので、より強くコントラストが強調されます。逆にの値を小さくすれば、坂の部分がなだらかになるので、コントラスト強調の度合いが和らぎます。
グラフを見るとおり、シグモイド関数を利用するとS字型のトーンカーブになります。それでは、GLSLで実装してみましょう。
GLSLでシグモイド関数を式のように用意します。
vec3 sigmoid(vec3 texture, float a) {
return 1.0 / (1.0 + exp(-a * (texture - 0.5)));
}第2引数のゲインを変えることで、画像の処理が変化します。
このシグモイド関数を利用したmain関数は次のようになります。
void main() {
vec2 uv = vUv;
vec4 texture = texture(uTexture, uv);
vec3 color = sigmoid(texture.rgb, gain);
fragColor = vec4(color, texture.a);
}シグモイド関数の第1引数にテクスチャのrgb値を渡すだけで、S字型のトーンカーブが適用されます。実装結果は次のようになります。デモでもゲインの値を変えられるので試してみてください!

特殊効果をもたらす色調変換
先程までの色調変換の関数は、主に画像の明度調整に利用されます。適用する関数しだいでは、さまざまな特殊効果をもたらす画像処理を行えます。ここでは、ネガ化処理・ソラリゼーション・ポスタリゼーションについて紹介します。
ネガ化処理

ネガ化処理は色の明暗を反転させ、ネガフィルムのように変化させるものです。
反転させるので次の式で実装できます。
GLSLで実装すると次のようになります。
void main() {
vec2 uv = vUv;
vec4 texture = texture(uTexture, uv);
vec3 color = 1.0 - texture.rgb;
fragColor = vec4(color, texture.a);
}ソラリゼーション

ソラリゼーションは色を急峡に変化させるような特殊効果を与える処理です。ある周期を持った関数なら、どのような関数を用いてもよいのですが、ここではsin波を用いて実装します。
式としては、次の式を使用します。
この式の意味はグラフを見てもらえると分かるかと思います。

要するに0~1の範囲で山が2つできるような関数を今回は使用して実装します。
GLSLのコードは次のようになります。
const float PI = 3.1415926;
vec3 solarization(vec3 texture) {
return sin(texture * PI * 2.0) * 0.6 + texture;
}
void main() {
vec2 uv = vUv;
vec4 texture = texture(uTexture, uv);
vec3 color = solarization(texture.rgb);
fragColor = vec4(color, texture.a);
}振幅や周期の値を変えることで、さまざまな特殊効果を得ることができるのでまずはグラフで波形を試しながら実装してみてください!
ポスタリゼーション

ポスタリゼーションは、画素値の階調を落とすような処理になります。次の図のようにトーンカーブは階段状になります。

階段の段数を少なくするほど階調が大きく落とされることになり、変化が大きくなります。写真などの自然画像にこの処理を施すと、元画像の細かな色の変化がなくなり、イラストのようになります。逆にイラストのようにもともと色の変化が少ない画像にこの処理を適用してもあまり大きな変化は得られません。
GLSLのコードは次のようになります。
vec3 posterization(vec3 texture, float level) {
return vec3(floor(texture * level) / (level - 1.0));
}階調の調整はuniformのlevelで調整できるようにしてるので、デモでぜひ試してみてください!
まとめ
GLSLで基本的な画像処理として、色調変換の方法を紹介しました。
デモではパラメータを変化できるようにしているので、ぜひ試してみてください!
次回は、GLSLで空間フィルタリングについて紹介します。
画像処理のおすすめ本
下記は画像処理全般の基礎の勉強におすすめの書籍になります。