HSL 色空間#

HSL 色空間は

  • Hue (色相)

  • Saturation (彩度)

  • Lightness (明度)

により色を表現する [1]

HSL 色空間は Joblove and Greenberg[2] が定義したものであり、 次のように HSL 色空間による表現 \((h, s, l)\) から RGB 色空間による表現 \((r, g, b)\) を計算する。

\[\begin{align*} r' &= \begin{cases} 1 & \text{if $0 \le h \le 1/6$ or $5/6 \le h < 1$} \\ 2 - 6h & \text{if $1/6 \le h \le 2/6$} \\ 0 & \text{if $2/6 \le h \le 4/6$} \\ 6h - 4 & \text{if $4/6 \le h \le 5/6$} \end{cases} \\ g' &= \begin{cases} 6h & \text{if $0 \le h \le 1/6$} \\ 1 & \text{if $1/6 \le h \le 3/6$} \\ 4 - 6h & \text{if $3/6 \le h \le 4/6$} \\ 0 & \text{if $4/6 \le h < 1$} \end{cases} \\ b' &= \begin{cases} 0 & \text{if $0 \le h \le 2/6$} \\ 6h - 2 & \text{if $2/6 \le h \le 3/6$} \\ 1 & \text{if $3/6 \le h \le 5/6$} \\ 6 - 6h & \text{if $5/6 \le h < 1$} \end{cases} \\ \begin{pmatrix} r \\ g \\ b \end{pmatrix} &= \begin{cases} \left( \begin{pmatrix} 0.5 \\ 0.5 \\ 0.5 \end{pmatrix} + s \left( \begin{pmatrix} r' \\ g' \\ b' \end{pmatrix} - \begin{pmatrix} 0.5 \\ 0.5 \\ 0.5 \end{pmatrix} \right) \right) \cdot 2i & \text{if $i \le 1/2$} \\ \begin{pmatrix} 0.5 \\ 0.5 \\ 0.5 \end{pmatrix} + s \left( \begin{pmatrix} r' \\ g' \\ b' \end{pmatrix} - \begin{pmatrix} 0.5 \\ 0.5 \\ 0.5 \end{pmatrix} \right) + \left( \begin{pmatrix} 0.5 \\ 0.5 \\ 0.5 \end{pmatrix} - s \left( \begin{pmatrix} r' \\ g' \\ b' \end{pmatrix} - \begin{pmatrix} 0.5 \\ 0.5 \\ 0.5 \end{pmatrix} \right) \right) (2i - 1) & \text{if $i \ge 1/2$} \end{cases} \end{align*}\]

Note

Joblove and Greenberg[2] の定式化では最後が \((2i - 1)\) でなく \((2 - 2i)\) だったが、 論文中の記述と矛盾するため \((2i - 1)\) とした。

HSL から RGB への変換については、 CSS Color Module Level 4[3] に JavaScript による実装例があるが、 ここでは、Python で実装して挙動を確認する。

色相に対する RGB の変化#

まず、\((r', g', b')\) の計算式を実装する。

Hide code cell content
%load_ext Cython
%%cython

cpdef double hue2r(double hue):
    """色相に対する RGB の R を計算する

    hue は [0, 1] の範囲にあるとする。
    """

    if (0.0 <= hue <= 1.0 / 6.0) or (5.0 / 6.0 <= hue):
        return 1.0
    elif 1.0 / 6.0 <= hue <= 2.0 / 6.0:
        return 2.0 - 6.0 * hue
    elif 2.0 / 6.0 <= hue <= 4.0 / 6.0:
        return 0.0
    else:
        return 6.0 * hue - 4.0

cpdef double hue2g(double hue):
    """色相に対する RGB の G を計算する

    hue は [0, 1] の範囲にあるとする。
    """

    if 0.0 <= hue <= 1.0 / 6.0:
        return 6 * hue
    elif 1.0 / 6.0 <= hue <= 3.0 / 6.0:
        return 1.0
    elif 3.0 / 6.0 <= hue <= 4.0 / 6.0:
        return 4.0 - 6.0 * hue
    else:
        return 0.0

cpdef double hue2b(double hue):
    """色相に対する RGB の B を計算する

    hue は [0, 1] の範囲にあるとする。
    """

    if 0.0 <= hue <= 2.0 / 6.0:
        return 0.0
    elif 2.0 / 6.0 <= hue <= 3.0 / 6.0:
        return 6.0 * hue - 2.0
    elif 3.0 / 6.0 <= hue <= 5.0 / 6.0:
        return 1.0
    else:
        return 6.0 - 6.0 * hue

これを用いて、色相に対する \((r', g', b')\) の挙動を以下に示す。

Hide code cell source
import numpy as np
import plotly.graph_objects as go

N = 121
h = np.linspace(0, 1, N)
r = np.vectorize(hue2r)(h)
g = np.vectorize(hue2g)(h)
b = np.vectorize(hue2b)(h)

rgb = np.concatenate((r, g, b))
rgb = np.reshape(rgb, (1, 3, N))
rgb = np.swapaxes(rgb, 1, 2)

fig = go.Figure()
fig.add_trace(go.Image(z=rgb * 255.0, dx=1.0 / (N - 1), dy=0.5, y0=1.5))
fig.add_trace(go.Scatter(x=h, y=r,
                            mode='lines', name="r'",
                            line={'color': 'red'}))
fig.add_trace(go.Scatter(x=h, y=g,
                            mode='lines', name="g'",
                            line={'color': 'green'}))
fig.add_trace(go.Scatter(x=h, y=b,
                            mode='lines', name="b'",
                            line={'color': 'blue'}))

fig.update_layout(title="色相に対する (r', g', b') の挙動")
fig.update_xaxes(range=[0.0, 1.0], title='色相')
fig.update_yaxes(range=[0.0, 1.75], scaleratio=0.4, title='RGB')
fig.show(renderer="notebook_connected")

色相、彩度、明度に対する色の変化#

続いて、彩度と明度も含めた色の計算を行う。

Hide code cell source
%%cython

# distutils: define_macros=NPY_NO_DEPRECATED_API=1

cimport cython
import numpy as np
cimport numpy as cnp

cdef double hue2r(double hue) nogil:
    """色相に対する RGB の R を計算する

    hue は [0, 1] の範囲にあるとする。
    """

    if (0.0 <= hue <= 1.0 / 6.0) or (5.0 / 6.0 <= hue):
        return 1.0
    elif 1.0 / 6.0 <= hue <= 2.0 / 6.0:
        return 2.0 - 6.0 * hue
    elif 2.0 / 6.0 <= hue <= 4.0 / 6.0:
        return 0.0
    else:
        return 6.0 * hue - 4.0

cdef double hue2g(double hue) nogil:
    """色相に対する RGB の G を計算する

    hue は [0, 1] の範囲にあるとする。
    """

    if 0.0 <= hue <= 1.0 / 6.0:
        return 6 * hue
    elif 1.0 / 6.0 <= hue <= 3.0 / 6.0:
        return 1.0
    elif 3.0 / 6.0 <= hue <= 4.0 / 6.0:
        return 4.0 - 6.0 * hue
    else:
        return 0.0

cdef double hue2b(double hue) nogil:
    """色相に対する RGB の B を計算する

    hue は [0, 1] の範囲にあるとする。
    """

    if 0.0 <= hue <= 2.0 / 6.0:
        return 0.0
    elif 2.0 / 6.0 <= hue <= 3.0 / 6.0:
        return 6.0 * hue - 2.0
    elif 3.0 / 6.0 <= hue <= 5.0 / 6.0:
        return 1.0
    else:
        return 6.0 - 6.0 * hue

cdef double _hsl2rgb_impl(double value, double s, double l) nogil:
    if l <= 0.5:
        return (0.5 + s * (value - 0.5)) * 2.0 * l
    else:
        return 0.5 + s * (value - 0.5) \
            + (0.5 - s * (value - 0.5)) * (2.0 * l - 1.0)

cpdef (double, double, double) hsl2rgb(double h, double s, double l) noexcept nogil:
    """HSL から RGB へ変換する
    """

    return (
        _hsl2rgb_impl(hue2r(h), s, l),
        _hsl2rgb_impl(hue2g(h), s, l),
        _hsl2rgb_impl(hue2b(h), s, l),
    )

@cython.boundscheck(False)
cpdef cnp.ndarray generate_rgb_array_for_hsl(Py_ssize_t N):
    """HSL に対応する RGB のプロットをするための配列を生成する
    """
    cdef cnp.ndarray hsl_values_array = np.linspace(0.0, 1.0, N)
    cdef double[:] hsl_values_view = hsl_values_array

    cdef cnp.ndarray rgb_array = np.zeros([N, N, N, 3], dtype=np.float64)
    cdef double[:, :, :, :] rgb_view = rgb_array

    cdef Py_ssize_t i, j, k
    cdef double h, s, l
    cdef (double, double, double) rgb

    for i in range(N):
        h = hsl_values_view[i]
        for j in range(N):
            s = hsl_values_view[j]
            for k in range(N):
                l = hsl_values_view[k]
                rgb = hsl2rgb(h, s, l)
                rgb_view[i, j, k, 0] = rgb[0]
                rgb_view[i, j, k, 1] = rgb[1]
                rgb_view[i, j, k, 2] = rgb[2]
    return rgb_array
Hide code cell source
import plotly.express as px
import xarray as xr

rgb = generate_rgb_array_for_hsl(N)
values = np.linspace(0.0, 1.0, N)
data = xr.DataArray(
        rgb,
        dims=['Hue', 'Saturation', 'Lightness', 'RGB'],
        coords=[
            ('Hue', values),
            ('Saturation', values),
            ('Lightness', values),
            ('RGB', ['R', 'G', 'B']),
        ])
data = data.transpose('Lightness', 'Saturation', 'Hue', 'RGB')

fig = px.imshow(data, animation_frame='Lightness',
            zmin=0.0, zmax=1.0,
            origin='lower',
            title='色相、彩度、明度に対する色の変化')
fig.show(renderer="notebook_connected")

Caution

アニメーションのバーを速く動かすと簡単に表示が崩れてしまう。

参考#