{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# HSL 色空間" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "HSL 色空間は\n", "\n", "- Hue (色相)\n", "- Saturation (彩度)\n", "- Lightness (明度)\n", "\n", "により色を表現する {footcite:p}`MozillaColor`。\n", "\n", "HSL 色空間は {footcite:t}`Joblove1978` が定義したものであり、\n", "次のように HSL 色空間による表現 $(h, s, l)$ から\n", "RGB 色空間による表現 $(r, g, b)$ を計算する。\n", "\n", "\\begin{align*}\n", " r' &= \\begin{cases}\n", " 1 & \\text{if $0 \\le h \\le 1/6$ or $5/6 \\le h < 1$} \\\\\n", " 2 - 6h & \\text{if $1/6 \\le h \\le 2/6$} \\\\\n", " 0 & \\text{if $2/6 \\le h \\le 4/6$} \\\\\n", " 6h - 4 & \\text{if $4/6 \\le h \\le 5/6$}\n", " \\end{cases}\n", " \\\\\n", " g' &= \\begin{cases}\n", " 6h & \\text{if $0 \\le h \\le 1/6$} \\\\\n", " 1 & \\text{if $1/6 \\le h \\le 3/6$} \\\\\n", " 4 - 6h & \\text{if $3/6 \\le h \\le 4/6$} \\\\\n", " 0 & \\text{if $4/6 \\le h < 1$}\n", " \\end{cases}\n", " \\\\\n", " b' &= \\begin{cases}\n", " 0 & \\text{if $0 \\le h \\le 2/6$} \\\\\n", " 6h - 2 & \\text{if $2/6 \\le h \\le 3/6$} \\\\\n", " 1 & \\text{if $3/6 \\le h \\le 5/6$} \\\\\n", " 6 - 6h & \\text{if $5/6 \\le h < 1$}\n", " \\end{cases}\n", " \\\\\n", " \\begin{pmatrix} r \\\\ g \\\\ b \\end{pmatrix} &=\n", " \\begin{cases}\n", " \\left(\n", " \\begin{pmatrix} 0.5 \\\\ 0.5 \\\\ 0.5 \\end{pmatrix}\n", " + s \\left(\n", " \\begin{pmatrix} r' \\\\ g' \\\\ b' \\end{pmatrix}\n", " - \\begin{pmatrix} 0.5 \\\\ 0.5 \\\\ 0.5 \\end{pmatrix}\n", " \\right)\n", " \\right) \\cdot 2i\n", " & \\text{if $i \\le 1/2$}\n", " \\\\\n", " \\begin{pmatrix} 0.5 \\\\ 0.5 \\\\ 0.5 \\end{pmatrix}\n", " + s \\left(\n", " \\begin{pmatrix} r' \\\\ g' \\\\ b' \\end{pmatrix}\n", " - \\begin{pmatrix} 0.5 \\\\ 0.5 \\\\ 0.5 \\end{pmatrix}\n", " \\right)\n", " + \\left(\n", " \\begin{pmatrix} 0.5 \\\\ 0.5 \\\\ 0.5 \\end{pmatrix}\n", " - s \\left(\n", " \\begin{pmatrix} r' \\\\ g' \\\\ b' \\end{pmatrix}\n", " - \\begin{pmatrix} 0.5 \\\\ 0.5 \\\\ 0.5 \\end{pmatrix}\n", " \\right)\n", " \\right) (2i - 1)\n", " & \\text{if $i \\ge 1/2$}\n", " \\end{cases}\n", "\\end{align*}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{note}\n", "{footcite:t}`Joblove1978`\n", "の定式化では最後が $(2i - 1)$ でなく $(2 - 2i)$ だったが、\n", "論文中の記述と矛盾するため $(2i - 1)$ とした。\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "HSL から RGB への変換については、\n", "{footcite:t}`W3CColor` に JavaScript による実装例があるが、\n", "ここでは、Python で実装して挙動を確認する。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 色相に対する RGB の変化\n", "\n", "まず、$(r', g', b')$ の計算式を実装する。" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-cell" ] }, "outputs": [], "source": [ "%load_ext Cython" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%cython\n", "\n", "cpdef double hue2r(double hue):\n", " \"\"\"色相に対する RGB の R を計算する\n", "\n", " hue は [0, 1] の範囲にあるとする。\n", " \"\"\"\n", "\n", " if (0.0 <= hue <= 1.0 / 6.0) or (5.0 / 6.0 <= hue):\n", " return 1.0\n", " elif 1.0 / 6.0 <= hue <= 2.0 / 6.0:\n", " return 2.0 - 6.0 * hue\n", " elif 2.0 / 6.0 <= hue <= 4.0 / 6.0:\n", " return 0.0\n", " else:\n", " return 6.0 * hue - 4.0\n", "\n", "cpdef double hue2g(double hue):\n", " \"\"\"色相に対する RGB の G を計算する\n", "\n", " hue は [0, 1] の範囲にあるとする。\n", " \"\"\"\n", "\n", " if 0.0 <= hue <= 1.0 / 6.0:\n", " return 6 * hue\n", " elif 1.0 / 6.0 <= hue <= 3.0 / 6.0:\n", " return 1.0\n", " elif 3.0 / 6.0 <= hue <= 4.0 / 6.0:\n", " return 4.0 - 6.0 * hue\n", " else:\n", " return 0.0\n", "\n", "cpdef double hue2b(double hue):\n", " \"\"\"色相に対する RGB の B を計算する\n", "\n", " hue は [0, 1] の範囲にあるとする。\n", " \"\"\"\n", "\n", " if 0.0 <= hue <= 2.0 / 6.0:\n", " return 0.0\n", " elif 2.0 / 6.0 <= hue <= 3.0 / 6.0:\n", " return 6.0 * hue - 2.0\n", " elif 3.0 / 6.0 <= hue <= 5.0 / 6.0:\n", " return 1.0\n", " else:\n", " return 6.0 - 6.0 * hue\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "これを用いて、色相に対する $(r', g', b')$ の挙動を以下に示す。" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "import numpy as np\n", "import plotly.graph_objects as go\n", "\n", "N = 121\n", "h = np.linspace(0, 1, N)\n", "r = np.vectorize(hue2r)(h)\n", "g = np.vectorize(hue2g)(h)\n", "b = np.vectorize(hue2b)(h)\n", "\n", "rgb = np.concatenate((r, g, b))\n", "rgb = np.reshape(rgb, (1, 3, N))\n", "rgb = np.swapaxes(rgb, 1, 2)\n", "\n", "fig = go.Figure()\n", "fig.add_trace(go.Image(z=rgb * 255.0, dx=1.0 / (N - 1), dy=0.5, y0=1.5))\n", "fig.add_trace(go.Scatter(x=h, y=r,\n", " mode='lines', name=\"r'\",\n", " line={'color': 'red'}))\n", "fig.add_trace(go.Scatter(x=h, y=g,\n", " mode='lines', name=\"g'\",\n", " line={'color': 'green'}))\n", "fig.add_trace(go.Scatter(x=h, y=b,\n", " mode='lines', name=\"b'\",\n", " line={'color': 'blue'}))\n", "\n", "fig.update_layout(title=\"色相に対する (r', g', b') の挙動\")\n", "fig.update_xaxes(range=[0.0, 1.0], title='色相')\n", "fig.update_yaxes(range=[0.0, 1.75], scaleratio=0.4, title='RGB')\n", "fig.show(renderer=\"notebook_connected\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 色相、彩度、明度に対する色の変化\n", "\n", "続いて、彩度と明度も含めた色の計算を行う。" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "%%cython\n", "\n", "# distutils: define_macros=NPY_NO_DEPRECATED_API=1\n", "\n", "cimport cython\n", "import numpy as np\n", "cimport numpy as cnp\n", "\n", "cdef double hue2r(double hue) nogil:\n", " \"\"\"色相に対する RGB の R を計算する\n", "\n", " hue は [0, 1] の範囲にあるとする。\n", " \"\"\"\n", "\n", " if (0.0 <= hue <= 1.0 / 6.0) or (5.0 / 6.0 <= hue):\n", " return 1.0\n", " elif 1.0 / 6.0 <= hue <= 2.0 / 6.0:\n", " return 2.0 - 6.0 * hue\n", " elif 2.0 / 6.0 <= hue <= 4.0 / 6.0:\n", " return 0.0\n", " else:\n", " return 6.0 * hue - 4.0\n", "\n", "cdef double hue2g(double hue) nogil:\n", " \"\"\"色相に対する RGB の G を計算する\n", "\n", " hue は [0, 1] の範囲にあるとする。\n", " \"\"\"\n", "\n", " if 0.0 <= hue <= 1.0 / 6.0:\n", " return 6 * hue\n", " elif 1.0 / 6.0 <= hue <= 3.0 / 6.0:\n", " return 1.0\n", " elif 3.0 / 6.0 <= hue <= 4.0 / 6.0:\n", " return 4.0 - 6.0 * hue\n", " else:\n", " return 0.0\n", "\n", "cdef double hue2b(double hue) nogil:\n", " \"\"\"色相に対する RGB の B を計算する\n", "\n", " hue は [0, 1] の範囲にあるとする。\n", " \"\"\"\n", "\n", " if 0.0 <= hue <= 2.0 / 6.0:\n", " return 0.0\n", " elif 2.0 / 6.0 <= hue <= 3.0 / 6.0:\n", " return 6.0 * hue - 2.0\n", " elif 3.0 / 6.0 <= hue <= 5.0 / 6.0:\n", " return 1.0\n", " else:\n", " return 6.0 - 6.0 * hue\n", "\n", "cdef double _hsl2rgb_impl(double value, double s, double l) nogil:\n", " if l <= 0.5:\n", " return (0.5 + s * (value - 0.5)) * 2.0 * l\n", " else:\n", " return 0.5 + s * (value - 0.5) \\\n", " + (0.5 - s * (value - 0.5)) * (2.0 * l - 1.0)\n", "\n", "cpdef (double, double, double) hsl2rgb(double h, double s, double l) noexcept nogil:\n", " \"\"\"HSL から RGB へ変換する\n", " \"\"\"\n", "\n", " return (\n", " _hsl2rgb_impl(hue2r(h), s, l),\n", " _hsl2rgb_impl(hue2g(h), s, l),\n", " _hsl2rgb_impl(hue2b(h), s, l),\n", " )\n", "\n", "@cython.boundscheck(False)\n", "cpdef cnp.ndarray generate_rgb_array_for_hsl(Py_ssize_t N):\n", " \"\"\"HSL に対応する RGB のプロットをするための配列を生成する\n", " \"\"\"\n", " cdef cnp.ndarray hsl_values_array = np.linspace(0.0, 1.0, N)\n", " cdef double[:] hsl_values_view = hsl_values_array\n", "\n", " cdef cnp.ndarray rgb_array = np.zeros([N, N, N, 3], dtype=np.float64)\n", " cdef double[:, :, :, :] rgb_view = rgb_array\n", "\n", " cdef Py_ssize_t i, j, k\n", " cdef double h, s, l\n", " cdef (double, double, double) rgb\n", "\n", " for i in range(N):\n", " h = hsl_values_view[i]\n", " for j in range(N):\n", " s = hsl_values_view[j]\n", " for k in range(N):\n", " l = hsl_values_view[k]\n", " rgb = hsl2rgb(h, s, l)\n", " rgb_view[i, j, k, 0] = rgb[0]\n", " rgb_view[i, j, k, 1] = rgb[1]\n", " rgb_view[i, j, k, 2] = rgb[2]\n", " return rgb_array" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "\n", "import plotly.express as px\n", "import xarray as xr\n", "\n", "rgb = generate_rgb_array_for_hsl(N)\n", "values = np.linspace(0.0, 1.0, N)\n", "data = xr.DataArray(\n", " rgb,\n", " dims=['Hue', 'Saturation', 'Lightness', 'RGB'],\n", " coords=[\n", " ('Hue', values),\n", " ('Saturation', values),\n", " ('Lightness', values),\n", " ('RGB', ['R', 'G', 'B']),\n", " ])\n", "data = data.transpose('Lightness', 'Saturation', 'Hue', 'RGB')\n", "\n", "fig = px.imshow(data, animation_frame='Lightness',\n", " zmin=0.0, zmax=1.0,\n", " origin='lower',\n", " title='色相、彩度、明度に対する色の変化')\n", "fig.show(renderer=\"notebook_connected\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{caution}\n", "アニメーションのバーを速く動かすと簡単に表示が崩れてしまう。\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 参考\n", "\n", "```{footbibliography}\n", "```" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 2 }