CSS3ブレンドモードのPython実装と色空間の話


CSS3のブレンドモードとは?

Photoshopなどの画像編集ツールに搭載されている「ブレンドモード(描画モード)」機能ですが、CSS3でもmix-blend-modeというプロパティで実装されています。
CSS3なので当然各ブラウザ(IE以外)で見た目を統一する必要があるので、W3Cの仕様に各ブレンドモードの計算式が載っています。

Compositing and Blending
Compositing and Blending 日本語訳

なお、策定にはAdobeの社員も関わっているらしく、おそらくですがPhotoshopなどのAdobe製品のブレンドモードと同じ計算式が使われているものと思われます。

意外と難しい"Non-separable blend"

W3Cの仕様を読み解いていくと、ブレンドモードは"Separable blend"と”Non-separable blend"の2つに分類されます。
”Non-separable blend"は、成分ごとに個別に扱わずに,すべての色成分の組合せを考慮するブレンドです。

"Separable blend"はscreenoverlayなど、単純な数式を使うものなので何の問題もありません。
問題は”Non-separable blend"に分類されるhue, saturation, color, luminosityの4つです。

例えばhueモードですが、ソース画像の色相(Hue)を、背景画像の輝度を維持しつつ適用するというモードです。
下の2つのイメージをhueモードで合成してみましょう。

合成結果は以下のとおりです。

「色相を移植するんだからHSV色空間に変換して、H成分を移植して・・・」という単純な話ではありません。
色相を移植しつつ、元画像の輝度(Luminance)を維持しなければなりません、つまり合成結果に対してモノクロ化すると以下のようにならなくてはいけないのです。

 

あひるが完全に消えましたね。

明度(Value)と明度(Lightness)と輝度(Luminance)の話

色相(Hue)を扱う色空間は、よく使われるのがHSV色空間とHSL色空間の2つです。
HSV色空間は色相(Hue)、彩度(Saturation)、明度(Value)の3で構成されます。

HSV色空間 -Wikipedia

HSL色空間は色相(Hue)、彩度(Saturation)、輝度(Lightness)の3つで構成されています。

HSL色空間 -Wikipedia

Wikipediaでは、HSL色空間の輝度(Lightness)と記載されていますが、これは輝度(Luminance)とは異なります。
ここらへんの単語の混乱は、いろいろなページで見られるので注意が必要です。

RGBから輝度(Luminance)へ変換する一般的な数式は以下のとおりです。

r * 0.298912 + g * 0.586611 + b * 0.114478

HSLのL(Lightness)への変換式は以下の通り。

(MAX(r, g, b) - MIN(r, g, b)) / 2

ハッキリいって全く違いますね。
Lightnessの訳は「明度」や「輝度」など資料によって訳され方がまちまちですので、混乱しないようにすることが重要です。

SetSatの解釈について

W3Cの計算式にSetSatという擬似コードの関数が記載されています。

SetSat(C, s)
    if(Cmax > Cmin)
        Cmid = (((Cmid - Cmin) x s) / (Cmax - Cmin))
        Cmax = s
    else
        Cmid = Cmax = 0
    Cmin = 0
    return C;

この擬似コードを最初に見た時は、意味がわかりませんでした。
CminはRGB成分の中の最小値、Cmaxは最大値、Cmidは中央値となります。

例を出すとC=RGB(0,8, 0.6, 0.3)とするなら、CminはB要素(0.3)、CmaxはR要素(0.8)、CmidはG要素(0.6)となります。
計算式は以下のとおりになります。

G = (((G - B) x s) / (R - B))
R = s
B = 0.0

という事になります。
ちなみにRGB(0.1, 0.1, 0.5)やRGB(0.5, 0.5, 0.1)のようにCminCmaxが2つある場合はどうなるかは書いていません。
正解はどうなるかはわかりませんが、以下の条件さえ満たせば、値は何でも良いようです。

max(Cred, Cgreen, Cblue) - min(Cred, Cgreen, Cblue) == s

Python(pillow)での実装

計算式がわかったので、Pythonの画像ライブラリであるpillowで実装します。pillowは実質的にPython標準の画像ライブラリと言って良いでしょう。OpenCVのように高度な機能はありませんが、様々なライブラリへデータコンバートが可能で、コンパクトで高速な画像処理が特徴です。

PIL/Pillow チートシートで記載されているように、ブレンドモードの実装はImageMathモジュールを使います。

例としてhard-lightの実装をしてみましょう。

from PIL import ImageMath


def _hard_light(a, b):
    _cl = 2 * a * b / 255
    _ch = 2.0 * (a + b - a * b / 255.0) - 255.0
    return _cl * (b < 128) + _ch * (b >= 128)

bands = []
for cb, cs in zip(backdrop.split(), source.split()):
    t = ImageMath.eval(
        "func(float(a), float(b))",
        func=_hard_light,
        a=cb, b=cs
    ).convert("L")
    bands += [t]

Image.merge("RGB", bands)

ImageMathモジュールを使うと、要素の計算を数値のように計算できるので便利です。
"Separable blend"のような単純な処理に関しては、こんな感じで実装ができます。

問題は"Non-separable blend"です、意外と計算量も多くトリッキーな処理もあり結構難しいです。
実装に関しては後でコードを公開していますので、興味がる方は見てもらうとして、ImageMathモジュールの非公開な関数を駆使して実装しています。
他言語で移植するのであれば、GLSLなどを使用すると思います、参考になれば幸いです。

「Image4Layer」モジュール

これらの処理を「Image4Layer」というモジュール名でパッケージ化しました。
画像処理系などでoverlayの合成などはよく使うので、結構使いドコロはあると思います。

インストールはpipで簡単に行なえます、実行にはpillow(PIL)があらかじめインストールされている必要があります。

$pip install image4layer

使い方は簡単です、color-dodgeモードで合成する例です。

from PIL import Image
from image4layer import Image4Layer

source = Image.open("ducky.png")
backdrop = Image.open("backdrop.png")

Image4Layer.color_dodge(backdrop, source)

簡単ですね、以下、対応しているブレンドモード一覧です。

ブレンドモード 画像
Image4Layer.normal
Image4Layer.multiply
Image4Layer.screen
Image4Layer.overlay
Image4Layer.darken
Image4Layer.lighten
Image4Layer.color_dodge
Image4Layer.color_burn
Image4Layer.hard_light
Image4Layer.soft_light
Image4Layer.difference
Image4Layer.exclusion
Image4Layer.hue
Image4Layer.saturation
Image4Layer.color
Image4Layer.luminosity

CSS3にはありませんが、Photoshopに搭載されているブレンドモードもついでに実装しました。

ブレンドモード 画像
Image4Layer.vivid_light
Image4Layer.pin_light
Image4Layer.linear_dodge
Image4Layer.subtract

ライセンスはMITですので、商用、個人利用とわず無料で使えます。

追記

Version0.4にPython2で動作しないことと、RGBA同士の演算にバグがありました、0.43で修正されています。

あとがき

Compositing and Blending 日本語訳には、想定される描画と実際にブラウザの表示があるのですが、Chromeでアクセスすると以下のようになります。

あれ・・・?色味が結構ちがくない?
ここら辺の表示は各ブラウザによって色味がかなり違ってきそうですね。

追記

Chromeのバージョンによって色味は違いそうです、私の環境はUbuntu環境で、どうやらbackdrop.pngに設定されているアルファ値の合成にバグがあるようです。

ちなみにImage4Lyaerの表示結果はChromeの結果とほぼ一緒です、おそくらく同じ実装をしているものと思います。
何か情報があれば、随時記事にしていきます。