はじめに
前回は、画像処理の擬似階調表現として組織的ディザ法をGLSLで実装しました。
今回はPhotoShopの機能やCSSのmix-blend-modeのような画像合成の処理をGLSLで実装してみます。
コードは下記のGitHubのリポジトリのsrc/canvasで公開しています。
画像合成とは
画像合成の基本は、ある画像のうえに別の画像を載せた状態を想定して処理を行うことです。PhotoShopなどのデザインツールのレイヤ機能やCSSのmix-blend-modeのような処理のことをいいます。この記事では次のような効果と処理の数式を紹介しながらGLSLで実装していきます。
| 効果 | 処理の例 |
|---|---|
| 明るくする効果 | 加算・スクリーン・覆い焼きカラー・比較(明) |
| 暗くする効果 | 乗算・ 焼き込みカラー・比較(暗) |
| コントラスト調整 | オーバーレイ・ソフトライト・ハードライト |
デモで使用する元画像と上にのせるレイヤー画像には次の2つの画像を使用します。

上記の比較用のレイヤー画像をGLSLで実装する方法は↓にありますので気になる方はご覧ください。
GLSLでのレイヤー画像の実装
上記の上半分が白黒のグラデーションと下半分がRGBのレイヤーをGLSLで書くと次のようになります。
vec3 red = vec3(1.0, 0.0, 0.0);
vec3 green = vec3(0.0, 1.0, 0.0);
vec3 blue = vec3(0.0, 0.0, 1.0);
vec3 layer(vec2 uv) {
vec3 topColor = vec3(uv.x);
vec3 bottomColor =
red * step(uv.x, 1.0 / 3.0) +
green * step(1.0 / 3.0, uv.x) * step(uv.x, 2.0 / 3.0) +
blue * step(2.0 / 3.0, uv.x);
return mix(topColor, bottomColor, step(uv.y, 0.5));
}まずは明るくする合成処理についてみていきましょう。
Important
GLSLの値は0.0から1.0までの範囲になるので、数式を紹介する際も画素値は0.0から1.0までの範囲として紹介します。
明るくする処理
合成した結果が元画像よりも明るくなる処理をみていきます。代表的なものに加算、スクリーン、覆い焼きカラー、比較(明)があります。
加算
2つの画素値を単純に足し合わせた値を出力します。加算しか行わないので合成された結果は必ず明るくなります。
上にある画像の画素値を、下にある画像の画素値をとし、合成結果をとすると、
単純な加算なので、処理結果が1.0を超える可能性があるので、超えた場合は最大値の1.0に補正する必要があります。
GLSLで加算の関数を書くと次のようになります。
vec3 blendAdd(vec3 base, vec3 blend) {
return min(base + blend, 1.0);
}この関数の使い方は次のようになります。
void main() {
vec2 uv = vUv;
vec3 baseTex = texture(uTexture, uv).rgb;
vec3 blendTex = layer(uv);
vec3 color = blendAdd(baseTex, blendTex);
fragColor = vec4(color, 1.0);
}結果は次のようになります。
単純な加算なので明るくなることが分かるでしょう。特に比較用のレイヤー画像との合成では、右上の白のグラデーション箇所がより強く光っているように見えます。また、黒は0になるため合成結果は元画像のままになることが分かります。

スクリーン
スクリーンでは、2つの画素値の反転(ネガ化)を取ってから乗算を行います。
計算式は次の通りです。
GLSLでスクリーンの関数を書くと次のようになります。
vec3 blendScreen(vec3 base, vec3 blend) {
return 1.0 - (1.0 - base) * (1.0 - blend);
}結果は次のようになります。
加算と同様に明るくなることが分かるかと思います。比較用のレイヤーとの合成を見てみると、中間部分の明るさが加算よりも弱く柔らかくなっていることが分かります。

覆い焼きカラー
覆い焼きカラーも全体を明るく変化させる処理ですが、合成する画像のが画素値を明るくする割合として利用します。計算式は次の通りです。
最初の式は、0での除算を防ぐためのもので、メインとなるのは下の式です。下の式では元画像を合成画像の反転で割った値を返します。
GLSLで覆い焼きカラーの関数を書くと次のようになります。
float blendColorDodge(float base, float blend) {
return (blend == 1.0) ? blend : (base / (1.0 - blend));
}
vec3 blendColorDodge(vec3 base, vec3 blend) {
return vec3(
blendColorDodge(base.r, blend.r),
blendColorDodge(base.g, blend.g),
blendColorDodge(base.b, blend.b)
);
}結果は次のようになります。
加算とよく似ていますが、基本色が暗い部分は合成後暗くなっています。加算の合成よりもコントラストが強くなっていることも分かります。

比較(明るい)
比較(明るい)では、元画像と合成画像の値を比較して、より大きい方の値を返します。
コードで見たほうがわかりやすいでしょう。この処理のGLSLの関数は次のようになります。
vec3 blendLighten(vec3 base, vec3 blend) {
return max(base, blend);
}max関数を利用して大きい値のほうを返しています。
結果は次のようになります。
比較(明るい)では、暗い箇所は使われないので、暗い部分を抜くような処理に利用されます。

暗くする処理
合成した結果が元画像よりも暗くなる処理をみていきます。代表的なものに乗算、焼き込みカラー、比較(暗)があります。
乗算
2つの画素値同士で乗算を行います。ただし暗くなる処理になりますので、乗算した結果を画素値の最大値で割る(正規化)する必要があります。乗算については説明がしやすいので、画素値の最大値を255と仮定してます。
例えば、元画像の値が150で、合成画像の値が100の場合、合成結果はとなり値は小さくなることが分かるでしょう。
GLSLで乗算を実装する際には、すでに値が0.0から1.0の範囲にあるため単純に掛け算するだけで大丈夫です。
vec3 blendMultiply(vec3 base, vec3 blend) {
return base * blend;
}結果は次のようになります。
加算とは逆に、乗算は白が無効化され、黒はそのまま保持されます。

焼き込みカラー
焼き込みカラーの処理は上記の覆い焼きカラーとは逆の処理になります。
計算式は次の通りです。
ここでも上の式では、0での除算を防ぐためのものになります。
GLSLで焼き込みカラーの関数を書くと次のようになります。
float blendColorBurn(float base, float blend) {
return blend == 0.0 ? blend : (1.0 - (1.0 - base) / blend);
}
vec3 blendColorBurn(vec3 base, vec3 blend) {
return vec3(
blendColorBurn(base.r, blend.r),
blendColorBurn(base.g, blend.g),
blendColorBurn(base.b, blend.b)
);
}結果は次のようになります。
覆い焼きカラーと逆の処理となるので、全体的に暗くなりコントラストが強くなることが確認できます。

比較(暗い)
比較(明るい)とは逆に、2つの画素値のうち、小さい方の値を返す処理になります。
vec3 blendDarken(vec3 base, vec3 blend) {
return min(base, blend);
}min関数を使用することで2つの値のうち小さい方の値を返すことができます。
結果は次のようになります。
比較(暗い)では白い部分が使われないことが分かるでしょう。このことから、明るい部分を抜くような処理に利用されます。

コントラスト調整
コントラストを調整する処理をみていきます。代表的なものにはオーバーレイ、ソフトライト、ハードライトがあります。
オーバーレイ
先述の乗算とスクリーンを組み合わせたような処理を行います。スクリーンの計算式は次の通りです。
この式のとおり、元画像()の割合が0.5より小さい場合は2を掛けて乗算し、0.5以上のときは元画像と合成画像を反転した値に2を掛けてから再度反転した値を返します。
GLSLでオーバーレイの関数を書くと次のようになります。
float blendOverlay(float base, float blend) {
return base < 0.5 ?
2.0 * base * blend :
1.0 - 2.0 * (1.0 - base) * (1.0 - blend);
}
vec3 blendOverlay(vec3 base, vec3 blend) {
return vec3(
blendOverlay(base.r, blend.r),
blendOverlay(base.g, blend.g),
blendOverlay(base.b, blend.b)
);
}結果は次のようになります。
元画像の暗い部分と明るい部分が生かされる処理になるので、コントラストが強くなるのが確認できるでしょう。

ハードライト
オーバーレイでは元画像()の値の割合によって、処理を分けていましたが、ハードライトでは合成画像()の値の割合によって、処理を分けます。その後の計算式はオーバーレイと同様になります。
GLSLでハードライトの関数を書くと次のようになります。
float blendHardLight(float base, float blend) {
return blend < 0.5 ?
2.0 * base * blend :
1.0 - 2.0 * (1.0 - base) * (1.0 - blend);
}
vec3 blendHardLight(vec3 base, vec3 blend) {
return vec3(
blendHardLight(base.r, blend.r),
blendHardLight(base.g, blend.g),
blendHardLight(base.b, blend.b)
);
}条件式を変えているだけなのが分かるでしょう。
結果は次のようになります。
同じ画像の合成ではオーバーレイと変化がないですが、比較用のレイヤー画像との合成では変化が見られるでしょう。

ソフトライト
ソフトライトでも合成画像()の値の割合によって、処理を分けます。複雑な式になりますが次の通りです。
GLSLでソフトライトの関数を書くと次のようになります。
float blendSoftLight(float base, float blend) {
return blend < 0.5 ?
2.0 * base * blend + base * base * (1.0 - 2.0 * blend):
2.0 * base * (1.0 - blend) + sqrt(base) * (2.0 * blend - 1.0);
}
vec3 blendSoftLight(vec3 base, vec3 blend) {
return vec3(
blendSoftLight(base.r, blend.r),
blendSoftLight(base.g, blend.g),
blendSoftLight(base.b, blend.b)
);
}結果は次のようになります。
計算式のとおり合成画像の割合によって処理を分けているのでハードライトと似ていますが、効果としてはハードライトとオーバーレイよりも弱く効いているのが分かるかと思います。

まとめ
PhotoShopなどのデザインツールのレイヤ機能やCSSのmix-blend-modeのような画像合成の処理を計算式を紹介しながら、GLSLで実装してみました。
デザインツールのレイヤ機能やCSSのmix-blend-modeにあるように、他にも画像合成の種類があるので気になる方は、調べてみて実装してみてください。ちなみに実装の参考にしたこちらには、GLSL実装例がたくさんあるのでおすすめです。
次回は、GLSLでKuwahara filterを実装してみたいと思います。
参考
以下の動画は、After Effectsでの描画モードで説明していますが、数式の説明や使用例まで丁寧に説明されているので分かりやすいと思います。この記事でも参考にさせてもらいました。
画像処理のおすすめ本
下記は画像処理全般の基礎の勉強におすすめの書籍になります。