{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Colour Grading" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAAFYklEQVR4nO3d0WrjRgBA0VHxQ///\nM9NQFhZKCwvqw9qNHSu2LDtNsvcchiKPpIki5W6TJ01jjDFN+3G8vTguH3Dn6V99/U9+eZ//At51\n/cVdz8+7cezy6s3htvzS47cBYQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoA\nSBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgbTfGGPOYxhjz\nGNOYDu8EGmOMaUzjAyZfPn6CydfH/F+T+5uzOHm669KRt6958nFxcs3F3Lrm2Y/EmycuHrl18q/v\nY7e/+fPRf42jMd7/SxgfNZ78CkScAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQ\nJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEjbXTtg3r72\n9NaCj3i9zXT8cTzyxTnTQ1c7vrzp0Qsu3427V3vA4uvu4c0rr340q1YeY/57N8YY07x/Ud5+TGdf\na3r98fyADae8/nauzqw4ZUw3HrBiZssph2/5hgNOZ64esOGUcfQsLs1sOOVoZvnjmpkNp4yzH62r\nBxzN/Pnj8H+Ak8d5/q688NvzVv+jY3y98eRvANoEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMA\naQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIE\nQNpujDG/vAhvmucxTWOapjH2G/u3bJ1OHjaWJ8eYLuxdfcqVdS6fcrR3cXLDKQuT1/bub+CKye3r\n3LJ3++Tpxp2TG9c531jcu37y27d/pp8/919wfNHLNjaM93vWT1/3V6D/8+7zsd7x+X7dAOABBECa\nAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQ\nJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSdh99AZvN1w95DC+J+XDv96znw9PdvzBmOmwfbby8o+l8\n7xuTY7q0d+0p19a5fMrL3sXJDacsTV7Ze7iB1yfvWOeGvXdMnmzcObl1nfONxb3rJ/cvyZuMt4f7\n8wuPJ38D0CYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBI\nEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0g4vyTt6Ddn038zhTTI/36p08nHN\nzC0HrJlZdcqNB6yZ2XDKGIdvefUBr2euHrDhlHH0LN6e2XDKyczSxzUzG04ZZz9aVw84nvnx58Kj\nfHUDL+29MqZHL/jG4g9bc+xvxwNXO7m8B6288P3esfL1u3f74mufyI0r3/CgV6w8xvhjvvaa1Lse\n1Xzf6R+4+Labbny18bu/AYgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECa\nAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGmHVyT9fJvcNL1s\nmPy0k4u73mPNO7/QnRf2Hmu+mvz+ffcyZSwO9+cXHk9PfgUiTQCkCYA0AZAmANIEQJoASBMAaQIg\nTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoA\nSBMAaQIgTQCk7Y4/zPN8+eh5nqe3jTEu7L3/gE++/ie/vM9/Ae+6/uKu5+fnfwE+nU1d/mn9vgAA\nAABJRU5ErkJggg==\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "% matplotlib inline\n", "\n", "from __future__ import division\n", "\n", "import IPython.display\n", "import IPython.html.widgets\n", "import PIL.Image\n", "import numpy as np\n", "import scipy.ndimage\n", "from cStringIO import StringIO\n", "import warnings\n", "\n", "import colour\n", "from colour.models.rgb.deprecated import RGB_to_HSV, HSV_to_RGB\n", "from colour.plotting import *\n", "\n", "warnings.filterwarnings('ignore')\n", "\n", "LUMINANCE_FACTORS = np.array([0.2126, 0.7152, 0.0722])\n", "\n", "IMAGE = colour.read_image('resources/images/Bands.exr')[..., 0:3]\n", "\n", "\n", "def array_plot(a, fmt='png'):\n", " buffer = StringIO()\n", "\n", " PIL.Image.fromarray(np.uint8(np.clip(a, 0, 1) * 255)).save(buffer, fmt)\n", " IPython.display.display(IPython.display.Image(data=buffer.getvalue()))\n", "\n", "\n", "def lerp(a, b, c):\n", " return (1 - c) * a + c * b\n", " \n", "\n", "def normal_distribution_function(a, mu=0.5, sigma=0.15):\n", " a = np.asarray(a)\n", "\n", " return np.exp(-np.power(a - mu, 2) / (2 * np.power(sigma, 2)))\n", "\n", "\n", "def contrast(a, contrast=1, pivot=0.18):\n", " a = np.asarray(a)\n", " contrast = np.asarray(contrast)\n", " pivot = np.asarray(pivot)\n", " \n", " return ((a / pivot) ** contrast) * pivot\n", "\n", "\n", "def glog(a, gain=1, lift=0, offset=0, gamma=1):\n", " a = np.asarray(a)\n", " gain = np.asarray(gain)\n", " lift = np.asarray(lift)\n", " offset = np.asarray(offset)\n", " gamma = np.asarray(gamma)\n", "\n", " a_o = (gain * a + offset + lift * (1 - a)) ** (1 / gamma)\n", "\n", " return a_o\n", "\n", "\n", "def saturation(a,\n", " saturation=1,\n", " luminance_factors=LUMINANCE_FACTORS):\n", " a = np.asarray(a)\n", " saturation = np.asarray(saturation)\n", "\n", " return lerp(a, np.dot(a, luminance_factors)[..., np.newaxis], 1 - saturation)\n", "\n", "\n", "def hue_rotate(a, angle):\n", " H, S, V = colour.tsplit(RGB_to_HSV(a))\n", "\n", " angle = angle / 360\n", "\n", " H += angle\n", " H[H > 1] -= 1\n", " H[H < 0] += 1\n", "\n", " return HSV_to_RGB(colour.tstack((H, S, V)))\n", "\n", "\n", "def colour_corrector(a,\n", " saturation_master=1,\n", " contrast_master=1,\n", " gain_master=1,\n", " lift_master=0,\n", " offset_master=0,\n", " gamma_master=1,\n", " saturation_shadows=1,\n", " contrast_shadows=1,\n", " gain_shadows=1,\n", " lift_shadows=0,\n", " offset_shadows=0,\n", " gamma_shadows=1,\n", " saturation_midtones=1,\n", " contrast_midtones=1,\n", " gain_midtones=1,\n", " lift_midtones=0,\n", " offset_midtones=0,\n", " gamma_midtones=1,\n", " saturation_highlights=1,\n", " contrast_highlights=1,\n", " gain_highlights=1,\n", " lift_highlights=0,\n", " offset_highlights=0,\n", " gamma_highlights=1,\n", " shadows_range=(0, 0.15),\n", " highlights_range=(1, 0.15),\n", " luminance_factors=LUMINANCE_FACTORS):\n", " Y = np.dot(a, luminance_factors)\n", "\n", " s_m = normal_distribution_function(Y, *shadows_range)\n", " h_m = normal_distribution_function(Y, *highlights_range)\n", " m_m = (1 - (s_m + h_m))\n", "\n", " i_m = glog(saturation(contrast(a, contrast_master), saturation_master, luminance_factors),\n", " gain_master, lift_master, offset_master, gamma_master)\n", "\n", " i_o = glog(saturation(contrast(i_m, contrast_shadows), saturation_shadows, luminance_factors),\n", " gain_shadows, \n", " lift_shadows, \n", " offset_shadows, \n", " gamma_shadows) * s_m[..., np.newaxis]\n", "\n", " i_o += glog(saturation(contrast(i_m, contrast_midtones), saturation_midtones, luminance_factors),\n", " gain_midtones, \n", " lift_midtones, \n", " offset_midtones,\n", " gamma_midtones) * m_m[..., np.newaxis]\n", "\n", " i_o += glog(saturation(contrast(i_m, contrast_highlights), saturation_highlights, luminance_factors),\n", " gain_highlights, \n", " lift_highlights, \n", " offset_highlights,\n", " gamma_highlights) * h_m[..., np.newaxis]\n", "\n", " return i_o\n", "\n", "\n", "def colour_corrector_interactive(a,\n", " saturation_master=1,\n", " contrast_master=1,\n", " gain_master=1,\n", " lift_master=0,\n", " offset_master=0,\n", " gamma_master=1,\n", " saturation_shadows=1,\n", " contrast_shadows=1,\n", " gain_shadows=1,\n", " lift_shadows=0,\n", " offset_shadows=0,\n", " gamma_shadows=1,\n", " saturation_midtones=1,\n", " contrast_midtones=1,\n", " gain_midtones=1,\n", " lift_midtones=0,\n", " offset_midtones=0,\n", " gamma_midtones=1,\n", " saturation_highlights=1,\n", " contrast_highlights=1,\n", " gain_highlights=1,\n", " lift_highlights=0,\n", " offset_highlights=0,\n", " gamma_highlights=1,\n", " s_ranges=0.15,\n", " h_ranges=0.15):\n", " array_plot(colour_corrector(\n", " a,\n", " saturation_master, contrast_master, gain_master, lift_master, offset_master, gamma_master, \n", " saturation_shadows, contrast_shadows, gain_shadows, lift_shadows, offset_shadows, gamma_shadows, \n", " saturation_midtones, contrast_midtones, gain_midtones, lift_midtones, offset_midtones, gamma_midtones,\n", " saturation_highlights, contrast_highlights, gain_highlights, lift_highlights, offset_highlights, gamma_highlights,\n", " shadows_range=(0, s_ranges), highlights_range=(1, h_ranges)))\n", "\n", "\n", "colour_corrector_widget = IPython.html.widgets.interactive(\n", " colour_corrector_interactive,\n", " a=IPython.html.widgets.fixed(IMAGE),\n", " saturation_master=(0, 1, 0.01),\n", " contrast_master=(0, 2, 0.01),\n", " gain_master=(-5, 5, 0.01),\n", " lift_master=(-5, 5, 0.01),\n", " offset_master=(-5, 5, 0.01),\n", " gamma_master=(0.01, 4, 0.01),\n", " saturation_shadows=(0, 1, 0.01),\n", " contrast_shadows=(0, 2, 0.01),\n", " gain_shadows=(-5, 5, 0.01),\n", " lift_shadows=(-5, 5, 0.01),\n", " offset_shadows=(-5, 5, 0.01),\n", " gamma_shadows=(0.01, 4, 0.01),\n", " saturation_midtones=(0, 1, 0.01),\n", " contrast_midtones=(0, 2, 0.01),\n", " gain_midtones=(-5, 5, 0.01),\n", " lift_midtones=(-5, 5, 0.01),\n", " offset_midtones=(-5, 5, 0.01),\n", " gamma_midtones=(0.01, 4, 0.01),\n", " saturation_highlights=(0, 1, 0.01),\n", " contrast_highlights=(0, 2, 0.01),\n", " gain_highlights=(-5, 5, 0.01),\n", " lift_highlights=(-5, 5, 0.01),\n", " offset_highlights=(-5, 5, 0.01),\n", " gamma_highlights=(0.01, 4, 0.01),\n", " s_ranges=(0.01, 1, 0.01),\n", " h_ranges=(0.01, 1, 0.01))\n", "\n", "\n", "categories = ('master', 'shadows', 'midtones', 'highlights', 'ranges')\n", "tab_widgets = []\n", "for category in categories:\n", " widgets = []\n", " for widget in colour_corrector_widget.children:\n", " if category in widget.description:\n", " widget.description = widget.description.split('_')[0].title()\n", " widgets.append(widget)\n", " \n", " tab_widgets.append(IPython.html.widgets.Box(children=widgets))\n", "\n", "tab_widget = IPython.html.widgets.Tab(children=tab_widgets)\n", "IPython.display.display(tab_widget)\n", "for i in range(len(categories)):\n", " tab_widget.set_title(i, categories[i].title())\n", "\n", "colour_corrector_widget.children = ()\n", "IPython.display.display(colour_corrector_widget)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import pylab\n", "from sympy.solvers import solve\n", "from sympy import Eq, Symbol\n", "\n", "\n", "def tonemapping_operator_Hottes2016(RGB, contrast, shoulder, b, c):\n", " RGB = np.asarray(RGB)\n", "\n", " z = RGB ** contrast\n", "\n", " return z / (z ** shoulder * b + c)\n", "\n", "\n", "CONTRAST = 1.25\n", "SHOULDER = 0.9\n", "\n", "B = Symbol('b')\n", "C = Symbol('c')\n", "\n", "\n", "f = lambda x: x ** CONTRAST / (x ** CONTRAST ** SHOULDER * B + C)\n", "\n", "solutions = solve((Eq(f(0.18), 0.35), Eq(f(1), 0.8)), B, C)\n", "\n", "print(solutions)\n", "\n", "samples = np.linspace(0, 1, 100)\n", "\n", "pylab.plot(\n", " samples,\n", " tonemapping_operator_Hottes2016(\n", " samples, CONTRAST, SHOULDER, solutions[B], solutions[C]))\n", "pylab.plot(samples, colour.oetf_BT709(samples))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "scrolled": false }, "outputs": [], "source": [ "def generate_RGB_tiles(size=128, repeat=4):\n", " samples = np.linspace(0, 1, size)\n", " R, G = np.meshgrid(samples, samples)\n", " B = np.zeros(R.shape)\n", " square = colour.tstack((R, G, B))\n", " \n", " tiles = np.tile(square, (repeat, repeat, 1)) \n", " \n", " B = np.linspace(0, 1, repeat * repeat).reshape((repeat, repeat))\n", " \n", " tiles[..., 2] = scipy.ndimage.zoom(B, size)\n", " \n", " return tiles\n", "\n", "\n", "tiles = generate_RGB_tiles() * 5\n", "array_plot(tiles)\n", "\n", "tiles_t = tonemapping_operator_Hottes2016(tiles, CONTRAST, SHOULDER, float(solutions[B]), float(solutions[C]))\n", "array_plot(tiles_t)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def physical_camera(a, FNumber=2.8, ExposureTime=1, ISO=100):\n", " a = np.asarray(a)\n", " \n", " EV = np.log2(FNumber ** 2) + np.log2(1 / ExposureTime) + np.log2(100 / ISO)\n", " \n", " return a * (1 / 2 ** EV)\n", "\n", "\n", "def physical_camera_interactive(a, \n", " FNumber=2.8, \n", " ExposureTime=1, \n", " ISO=100):\n", " array_plot(physical_camera(a, FNumber, ExposureTime, ISO))\n", "\n", " \n", "\n", "IPython.html.widgets.interactive(\n", " physical_camera_interactive,\n", " a=IPython.html.widgets.fixed(IMAGE),\n", " FNumber=(1, 22, 0.1),\n", " ExposureTime=(0.01, 30, 0.01), \n", " ISO=(50, 1600, 50))" ] } ], "metadata": { "kernelspec": { "display_name": "Python 2", "language": "python", "name": "python2" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.11" } }, "nbformat": 4, "nbformat_minor": 0 }