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')\) の計算式を実装する。
Show 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')\) の挙動を以下に示す。
Show 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")
色相、彩度、明度に対する色の変化#
続いて、彩度と明度も含めた色の計算を行う。
Show 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
Show 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
アニメーションのバーを速く動かすと簡単に表示が崩れてしまう。