{ "metadata": { "name": "", "signature": "sha256:d07c3ef3471b341c2921bc37363fcbd0d2c6b9dc2ab7f9061c370cc77b8f72b0" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Spectral to RGB" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is some **.csv** spectral data of high interest (they are in meters): https://github.com/colour-science/colour-ocean" ] }, { "cell_type": "code", "collapsed": false, "input": [ "%matplotlib inline" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 1 }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Spectral Data" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import numpy as np\n", "import colour\n", "\n", "COLOUR_RENDITION_CHART = colour.COLOURCHECKERS_SPDS['ColorChecker N Ohta']\n", "\n", "SAMPLE_SPD = COLOUR_RENDITION_CHART['neutral 5 (.70 D)']\n", "CMFS = colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']\n", "ILLUMINANT = colour.ILLUMINANTS_RELATIVE_SPDS['D65']" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Spectral to Tristimulus Values" ] }, { "cell_type": "code", "collapsed": false, "input": [ "XYZ = colour.spectral_to_XYZ(SAMPLE_SPD, CMFS, ILLUMINANT)\n", "print(XYZ)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "[ 19.31022942 20.30535098 22.15676876]\n" ] } ], "prompt_number": 3 }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Annotated Implementation Details" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*colour.spectral_to_XYZ* definition is self-contained and doesn't have any dependencies on other objects except for some methods of *colour.SpectralPowerDistribution* class.\n", "\n", "> note: The computation domain is [0, 100]." ] }, { "cell_type": "code", "collapsed": false, "input": [ "def spectral_to_XYZ(spd,\n", " cmfs=STANDARD_OBSERVERS_CMFS.get(\n", " 'CIE 1931 2 Degree Standard Observer'),\n", " illuminant=None):\n", " \"\"\"\n", " Converts given spectral power distribution to *CIE XYZ* tristimulus values\n", " using given colour matching functions and illuminant.\n", "\n", " Parameters\n", " ----------\n", " spd : SpectralPowerDistribution\n", " Spectral power distribution.\n", " cmfs : XYZ_ColourMatchingFunctions\n", " Standard observer colour matching functions.\n", " illuminant : SpectralPowerDistribution, optional\n", " *Illuminant* spectral power distribution.\n", "\n", " Returns\n", " -------\n", " ndarray, (3,)\n", " *CIE XYZ* tristimulus values.\n", "\n", " Warning\n", " -------\n", " The output domain of that definition is non standard!\n", "\n", " Notes\n", " -----\n", " - Output *CIE XYZ* tristimulus values are in domain [0, 100].\n", "\n", " References\n", " ----------\n", " .. [1] Wyszecki, G., & Stiles, W. S. (2000). Integration Replace by\n", " Summation. In Color Science: Concepts and Methods, Quantitative\n", " Data and Formulae (pp. 158\u2013163). Wiley. ISBN:978-0471399186\n", "\n", " Examples\n", " --------\n", " >>> from colour import CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution # noqa\n", " >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer')\n", " >>> data = {380: 0.0600, 390: 0.0600}\n", " >>> spd = SpectralPowerDistribution('Custom', data)\n", " >>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50')\n", " >>> spectral_to_XYZ(spd, cmfs, illuminant) # doctest: +ELLIPSIS\n", " array([ 4.5764852...e-04, 1.2964866...e-05, 2.1615807...e-03])\n", " \"\"\"\n", " \n", " # http://nbviewer.jupyter.org/github/colour-science/colour-notebooks/blob/master/notebooks/colorimetry/spectrum.ipynb#CIE-XYZ-Tristimulus-Values\n", "\n", " # This first block ensures that we have all the spectral data available and with matching\n", " # spectral shape (start, end, steps), `CMFS` will be the spectral shape reference.\n", " shape = cmfs.shape\n", " if spd.shape != cmfs.shape:\n", " # If `spd` argument has a different shape than `CMFS` argument, we fill it with *0*.\n", " # We clone it before because a lot of operations on spectral power distributions \n", " # happen in place and we want to keep the original one vanilla.\n", " spd = spd.clone().zeros(shape)\n", "\n", " if illuminant is None:\n", " # No `illuminant` argument has been provided thus we use a *1* filled illuminant instead.\n", " # Cases where you don't provide an illuminant are when your input `spd` is an\n", " # actual light source or illuminant. \n", " illuminant = ones_spd(shape)\n", " else:\n", " if illuminant.shape != cmfs.shape:\n", " # If `illuminant` argument has a different shape than `CMFS` argument, we fill it with *0*.\n", " illuminant = illuminant.clone().zeros(shape)\n", "\n", " # We retrieve the actual spectral data that is now aligned with the *CMFS* shape.\n", " spd = spd.values\n", " x_bar, y_bar, z_bar = (cmfs.x_bar.values,\n", " cmfs.y_bar.values,\n", " cmfs.z_bar.values)\n", " illuminant = illuminant.values\n", " \n", " # Follows the integral implementation as a summation. \n", " \n", " # Products. \n", " x_products = spd * x_bar * illuminant\n", " y_products = spd * y_bar * illuminant\n", " z_products = spd * z_bar * illuminant\n", "\n", " # *CIE* uses a [0, 100] computation domain for the spectral to tristimulus values conversion.\n", " # and normalises the Luminance with a *100* factor. \n", " normalising_factor = 100 / np.sum(y_bar * illuminant)\n", "\n", " # Summation. \n", " XYZ = np.array([normalising_factor * np.sum(x_products),\n", " normalising_factor * np.sum(y_products),\n", " normalising_factor * np.sum(z_products)])\n", "\n", " return XYZ" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Tristimulus Values to Linear sRGB" ] }, { "cell_type": "code", "collapsed": false, "input": [ "RGB = colour.XYZ_to_sRGB(XYZ / 100, transfer_function=False)\n", "print(RGB)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "[ 0.20319374 0.20296181 0.20354896]\n" ] } ], "prompt_number": 4 }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Annotated Implementation Details" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*colour.XYZ_to_sRGB* definition is a convenient wrapper around *colour.XYZ_to_RGB*.\n", "\n", "> note: The computation domain is [0, 1]." ] }, { "cell_type": "code", "collapsed": false, "input": [ "def XYZ_to_sRGB(XYZ,\n", " illuminant=RGB_COLOURSPACES.get('sRGB').whitepoint,\n", " chromatic_adaptation_transform='CAT02',\n", " transfer_function=True):\n", " \"\"\"\n", " Converts from *CIE XYZ* tristimulus values to *sRGB* colourspace.\n", "\n", " Parameters\n", " ----------\n", " XYZ : array_like\n", " *CIE XYZ* tristimulus values.\n", " illuminant : array_like, optional\n", " Source illuminant chromaticity coordinates.\n", " chromatic_adaptation_transform : unicode, optional\n", " {'CAT02', 'XYZ Scaling', 'Von Kries', 'Bradford', 'Sharp', 'Fairchild,\n", " 'CMCCAT97', 'CMCCAT2000', 'CAT02_BRILL_CAT', 'Bianco', 'Bianco PC'},\n", " *Chromatic adaptation* transform.\n", " transfer_function : bool, optional\n", " Apply *sRGB* *opto-electronic conversion function*.\n", "\n", " Returns\n", " -------\n", " ndarray\n", " *sRGB* colour array.\n", "\n", " Notes\n", " -----\n", " - Input *CIE XYZ* tristimulus values are in domain [0, 1].\n", "\n", " Examples\n", " --------\n", " >>> import numpy as np\n", " >>> XYZ = np.array([0.07049534, 0.10080000, 0.09558313])\n", " >>> XYZ_to_sRGB(XYZ) # doctest: +ELLIPSIS\n", " array([ 0.1750135..., 0.3881879..., 0.3216195...])\n", " \"\"\"\n", "\n", " sRGB = RGB_COLOURSPACES.get('sRGB')\n", "\n", " # Nothing fancy here, just passing relevant arguments to `colour.XYZ_to_RGB`.\n", " # The `illuminant` argument is used to perform chromatic adaptation between\n", " # the illuminant used for the spectral to tristimulus values conversion and\n", " # sRGB *D65* whitepoint. Default is to assume that `illuminant` argument is\n", " # *D65*, thus discounting chromatic adaptation entirely. \n", " return XYZ_to_RGB(XYZ,\n", " illuminant,\n", " sRGB.whitepoint,\n", " sRGB.XYZ_to_RGB_matrix,\n", " chromatic_adaptation_transform,\n", " sRGB.transfer_function if transfer_function else None)\n", "\n", "\n", "def XYZ_to_RGB(XYZ,\n", " illuminant_XYZ,\n", " illuminant_RGB,\n", " XYZ_to_RGB_matrix,\n", " chromatic_adaptation_transform='CAT02',\n", " transfer_function=None):\n", " \"\"\"\n", " Converts from *CIE XYZ* tristimulus values to given *RGB* colourspace.\n", "\n", " Parameters\n", " ----------\n", " XYZ : array_like\n", " *CIE XYZ* tristimulus values.\n", " illuminant_XYZ : array_like\n", " *CIE XYZ* tristimulus values *illuminant* *xy* chromaticity\n", " coordinates.\n", " illuminant_RGB : array_like\n", " *RGB* colourspace *illuminant* *xy* chromaticity coordinates.\n", " XYZ_to_RGB_matrix : array_like\n", " *Normalised primary matrix*.\n", " chromatic_adaptation_transform : unicode, optional\n", " {'CAT02', 'XYZ Scaling', 'Von Kries', 'Bradford', 'Sharp', 'Fairchild,\n", " 'CMCCAT97', 'CMCCAT2000', 'CAT02_BRILL_CAT', 'Bianco', 'Bianco PC'},\n", " *Chromatic adaptation* transform.\n", " transfer_function : object, optional\n", " *Opto-electronic conversion function*.\n", "\n", " Returns\n", " -------\n", " ndarray\n", " *RGB* colourspace array.\n", "\n", " Notes\n", " -----\n", " - Input *CIE XYZ* tristimulus values are in domain [0, 1].\n", " - Input *illuminant_XYZ* *xy* chromaticity coordinates are in domain\n", " [0, 1].\n", " - Input *illuminant_RGB* *xy* chromaticity coordinates are in domain\n", " [0, 1].\n", " - Output *RGB* colourspace array is in domain [0, 1].\n", "\n", " Examples\n", " --------\n", " >>> XYZ = np.array([0.07049534, 0.10080000, 0.09558313])\n", " >>> illuminant_XYZ = np.array([0.34567, 0.35850])\n", " >>> illuminant_RGB = np.array([0.31271, 0.32902])\n", " >>> chromatic_adaptation_transform = 'Bradford'\n", " >>> XYZ_to_RGB_matrix = np.array([\n", " ... [3.24100326, -1.53739899, -0.49861587],\n", " ... [-0.96922426, 1.87592999, 0.04155422],\n", " ... [0.05563942, -0.20401120, 1.05714897]])\n", " >>> XYZ_to_RGB(\n", " ... XYZ,\n", " ... illuminant_XYZ,\n", " ... illuminant_RGB,\n", " ... XYZ_to_RGB_matrix,\n", " ... chromatic_adaptation_transform) # doctest: +ELLIPSIS\n", " array([ 0.0110360..., 0.1273446..., 0.1163103...])\n", " \"\"\"\n", "\n", " # The first step for conversion from *CIE XYZ* to *RGB* is to compute\n", " # the chromatic adaptation matrix, I would actually suggest to always\n", " # account for it in any *CIE XYZ* to *RGB* conversion code even if \n", " # you don't need it, thus the day the whitepoints / illuminants are\n", " # different, nothing will break. \n", " M = chromatic_adaptation_matrix_VonKries(\n", " xy_to_XYZ(illuminant_XYZ),\n", " xy_to_XYZ(illuminant_RGB),\n", " transform=chromatic_adaptation_transform)\n", "\n", " XYZ_a = dot_vector(M, XYZ)\n", "\n", " # XYZ_to_RGB_matrix is the inverse of the NPM and can be computed using\n", " # `colour.normalised_primary_matrix` or taking the inverse of \n", " # https://www.colour-science.org/cgi-bin/rgb_colourspace_models_derivation.cgi\n", " RGB = dot_vector(XYZ_to_RGB_matrix, XYZ_a)\n", "\n", " if transfer_function is not None:\n", " RGB = transfer_function(RGB)\n", "\n", " return RGB\n", "\n", "\n", "def chromatic_adaptation_matrix_VonKries(XYZ_w, XYZ_wr, transform='CAT02'):\n", " \"\"\"\n", " Computes the *chromatic adaptation* matrix from test viewing conditions\n", " to reference viewing conditions.\n", "\n", " Parameters\n", " ----------\n", " XYZ_w : array_like\n", " Test viewing condition *CIE XYZ* tristimulus values of whitepoint.\n", " XYZ_wr : array_like\n", " Reference viewing condition *CIE XYZ* tristimulus values of whitepoint.\n", " transform : unicode, optional\n", " {'CAT02', 'XYZ Scaling', 'Von Kries', 'Bradford', 'Sharp', 'Fairchild,\n", " 'CMCCAT97', 'CMCCAT2000', 'CAT02_BRILL_CAT', 'Bianco', 'Bianco PC'},\n", " Chromatic adaptation transform.\n", "\n", " Returns\n", " -------\n", " ndarray\n", " Chromatic adaptation matrix.\n", "\n", " Raises\n", " ------\n", " KeyError\n", " If chromatic adaptation method is not defined.\n", "\n", " Examples\n", " --------\n", " >>> XYZ_w = np.array([1.09846607, 1.00000000, 0.35582280])\n", " >>> XYZ_wr = np.array([0.95042855, 1.00000000, 1.08890037])\n", " >>> chromatic_adaptation_matrix_VonKries(XYZ_w, XYZ_wr) # noqa # doctest: +ELLIPSIS\n", " array([[ 0.8687653..., -0.1416539..., 0.3871961...],\n", " [-0.1030072..., 1.0584014..., 0.1538646...],\n", " [ 0.0078167..., 0.0267875..., 2.9608177...]])\n", "\n", " Using Bradford method:\n", "\n", " >>> XYZ_w = np.array([1.09846607, 1.00000000, 0.35582280])\n", " >>> XYZ_wr = np.array([0.95042855, 1.00000000, 1.08890037])\n", " >>> method = 'Bradford'\n", " >>> chromatic_adaptation_matrix_VonKries(XYZ_w, XYZ_wr, method) # noqa # doctest: +ELLIPSIS\n", " array([[ 0.8446794..., -0.1179355..., 0.3948940...],\n", " [-0.1366408..., 1.1041236..., 0.1291981...],\n", " [ 0.0798671..., -0.1349315..., 3.1928829...]])\n", " \"\"\"\n", "\n", " # http://nbviewer.jupyter.org/github/colour-science/colour-notebooks/blob/master/notebooks/adaptation/vonkries.ipynb\n", " # http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html \n", " M = CHROMATIC_ADAPTATION_TRANSFORMS.get(transform)\n", "\n", " if M is None:\n", " raise KeyError(\n", " '\"{0}\" chromatic adaptation transform is not defined! Supported '\n", " 'methods: \"{1}\".'.format(transform,\n", " CHROMATIC_ADAPTATION_TRANSFORMS.keys()))\n", "\n", " rgb_w = np.einsum('...i,...ij->...j', XYZ_w, np.transpose(M))\n", " rgb_wr = np.einsum('...i,...ij->...j', XYZ_wr, np.transpose(M))\n", "\n", " D = rgb_wr / rgb_w\n", "\n", " D = row_as_diagonal(D)\n", "\n", " cat = dot_matrix(np.linalg.inv(M), D)\n", " cat = dot_matrix(cat, M)\n", "\n", " return cat" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Linear sRGB to *Gamma sRGB* 8 bit" ] }, { "cell_type": "code", "collapsed": false, "input": [ "RGB = (colour.RGB_COLOURSPACES['sRGB'].transfer_function(RGB) * 255).astype(np.int_)\n", "print(RGB)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "[124 124 124]\n" ] } ], "prompt_number": 5 } ], "metadata": {} } ] }