diff --git a/exposure_triangle.ipynb b/exposure_triangle.ipynb index c135e35..770b584 100644 --- a/exposure_triangle.ipynb +++ b/exposure_triangle.ipynb @@ -1,290 +1,285 @@ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%matplotlib widget\n", "\n", "from PIL import Image, ImageFilter, ImageEnhance, ImageDraw, ImageFont\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", "class parameter:\n", " \"\"\"\n", " Base class for parameters.\n", " The scale is a dict that links the steps (from the slider) to a real value.\n", " \"\"\"\n", " def __init__(self, scale, step = 0):\n", " self.step = None\n", " self.scale = scale\n", " self.update(step)\n", " \n", " def update(self, step = None):\n", " if step is not None:\n", " if step in self.scale:\n", " self.step = step\n", " pass\n", " \n", " def getEvValue(self):\n", " return self.step\n", " \n", " def getValue(self):\n", " if self.step in self.scale:\n", " return self.scale[self.step]\n", " return None\n", "\n", "\n", "class blurImage(parameter):\n", " \"\"\"\n", " A 'parameter' class that blurs a base image, based on the current step.\n", " Adds methods to deal with the images.\n", " \"\"\"\n", " def __init__(self,\n", " scale = {0: 1/500, 1: 1/250, 2: 1/125, 3: 1/60, 4: 1/30, 5: 1/15, 6: 1/8, 7: 1/3, 8: 1/2, 9: 1},\n", " step = 0,\n", " baseImage = None):\n", " super().__init__(scale, step = step)\n", " self.images = {}\n", " if baseImage is not None: self.images[0] = Image.open(baseImage)\n", " self._initImages()\n", " \n", " def getImage(self):\n", " return self.images[self.step] \n", "\n", " def _initImages(self):\n", " for step in range(1, 10):\n", " self.images[step] = self.images[step-1].filter(ImageFilter.BLUR)\n", " \n", "class shutterSpeed(blurImage):\n", " \"\"\"\n", " 'shutterSpeed' class. It blurs the base image, based on the shutter speed.\n", " \"\"\"\n", " def __init__(self,\n", " scale = {0: 1/500, 1: 1/250, 2: 1/125, 3: 1/60, 4: 1/30, 5: 1/15, 6: 1/8, 7: 1/3, 8: 1/2, 9: 1},\n", " baseImage = 'images/flying.png'):\n", " super().__init__(scale, 0, baseImage)\n", "\n", "class aperture(blurImage):\n", " \"\"\"\n", " 'aperture' class. It blurs the background image, based on the aperture.\n", " \"\"\"\n", " def __init__(self,\n", " scale = {0: 22, 1: 16, 2: 11, 3: 8, 4: 5.6, 5: 4, 6: 2.8, 7: 2, 8: 1.4, 9: 1},\n", " baseImage = 'images/background.jpg'):\n", " super().__init__(scale, 0, baseImage)\n", "\n", "class ISO(parameter):\n", " \"\"\"\n", " 'ISO' class is a simple 'parameter' class with a specific scale.\n", " It also provides a function to get the grain 'percentage' to apply to the final image\n", " \"\"\"\n", " def __init__(self, scale = {0: 50, 1: 100, 2: 200, 3: 400, 4: 800, 5: 1600, 6: 3200, 7: 6400, 8: 12800, 9: 25600}):\n", " super().__init__(scale, step = 0)\n", " \n", " def getGrainPercentage(self):\n", " return self.step*50./9\n", " \n", "class weather(parameter):\n", " \"\"\"\n", " 'weather' class. The scale has 3 values 0/1/2 corresponding to sunny/partially/cloudy\n", " that affect the EV (0/-5/-10)\n", " \"\"\"\n", " def __init__(self, scale = {0: 0, 1: -5, 2: -10}):\n", " super().__init__(scale, step = 0)\n", " \n", " def getEvValue(self):\n", " return self.scale[self.step]\n", " \n", " def getPrettyName(self):\n", " v = {0: 'sunny', 1: 'partially cloudy', 2: 'cloudy'}\n", " return v[self.step]\n", " \n", "class exposureTriangle:\n", " \"\"\"\n", " Main class of the exposire triangle.\n", " It instantiates the shutterSpeed, Aperture, ISO and weather objects\n", " \"\"\"\n", " def __init__(self):\n", " self.spd = shutterSpeed()\n", " self.ape = aperture()\n", " self.iso = ISO()\n", " self.wea = weather()\n", " self.image = None\n", " self.updateImage()\n", " self.redo_image = True\n", " \n", " def calculateEv(self):\n", " EV=self.spd.getEvValue()+self.ape.getEvValue()+self.iso.getEvValue()+self.wea.getEvValue()-4\n", " return int(EV)\n", " \n", " def setWea(self, step):\n", " self.wea.update(step)\n", " self.redo_image = True\n", " \n", " def setSpd(self, step):\n", " self.spd.update(step)\n", " self.redo_image = True\n", "\n", " def setApe(self, step):\n", " self.ape.update(step)\n", " self.redo_image = True\n", " \n", " def setIso(self, step=1):\n", " self.iso.update(step)\n", " self.redo_image = True\n", "\n", " def updateImage(self):\n", " # Place background (from Aperture)\n", " self.image = self.ape.getImage()\n", " # Place foreground (from ShutterSpeed)\n", " self.image.paste(self.spd.getImage(), (0, 0), self.spd.getImage())\n", " # Add ISO grain\n", " self._addIsoGrain(self.iso.getGrainPercentage())\n", " # Correct brightness (based on EV value)\n", " ev = self.calculateEv()\n", " ev_text = 'EV: '\n", " if ev <= 0:\n", " ev_text += str(ev)\n", - " if ev < 0:\n", - " #ev = ev -1\n", - " #ev=-1./ev\n", - " pass\n", " else:\n", " ev_text += '+'+str(ev)\n", - " #ev += 1\n", " if ev != 0:\n", " bright = ImageEnhance.Brightness(self.image)\n", " self.image = bright.enhance(2**(ev/3))\n", " # Add EV (overlay)\n", " font = ImageFont.truetype(r'DejaVuSans-Bold.ttf', 16)\n", " ImageDraw.Draw(self.image).text((10, 10), ev_text, (255, 0, 0), font = font)\n", " self.redo_image = False\n", " \n", " def getImage(self):\n", " if self.redo_image: self.updateImage()\n", " return self.image\n", "\n", " def prettyString(self):\n", " ps = \"Current settings:
\"\n",
     "        ps += \"Weather          : {}
\".format(self.wea.getPrettyName())\n", " ps += \"Shutter speed (s): 1/{}
\".format(int(1/self.spd.getValue()))\n", " ps += \"Aperture : f/{}
\".format(self.ape.getValue())\n", " ps += \"ISO : {}
\".format(self.iso.getValue())\n", " ps += \"EV : {}
\".format(self.calculateEv())\n", " return ps\n", " \n", " def prettyPrint(self):\n", " print(self.prettyString())\n", " \n", " def _addIsoGrain(self, amount, shift = 1.1):\n", " output = np.copy(np.array(self.image))\n", " x_size, y_size, z_size = output.shape\n", " pixels = (np.random.rand(x_size, y_size)*100/(100-amount)).astype(int)\n", " it = np.nditer(pixels, flags=['multi_index'])\n", " while not it.finished:\n", " if it[0]:\n", " output[it.multi_index] = [min(output[it.multi_index][0]*shift, 255),\n", " min(output[it.multi_index][1]*shift, 255),\n", " min(output[it.multi_index][2]*shift, 255)]\n", " it.iternext()\n", " self.image = Image.fromarray(output)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's build a minimalist UI\n", "\n", "from IPython.display import display, clear_output, display_pretty, display_jpeg, display_html\n", "from ipywidgets import widgets, HBox, VBox\n", "\n", "# First, we need a triangle :-)\n", "tri = exposureTriangle()\n", "\n", "# We need an output (to display the resulting image)\n", "output = widgets.Output()\n", "# We need an HTML widget to displayt the current settings\n", "html = widgets.HTML()\n", "\n", "# This function will be notified of any change in the settings\n", "# and will calculate a new image and display it\n", "def on_change(change):\n", " with output:\n", " new = change['new']\n", " if change['owner'].description == 'Shut. Speed':\n", " tri.setSpd(new)\n", " elif change['owner'].description == 'Aperture':\n", " tri.setApe(new)\n", " elif change['owner'].description == 'ISO':\n", " tri.setIso(new)\n", " elif change['owner'].description == 'Weather':\n", " tri.setWea(new)\n", " else:\n", " pass\n", " new_img = tri.getImage()\n", " clear_output(wait=True)\n", " display(new_img)\n", " html.value = tri.prettyString()\n", "\n", "# We need sliders to control the triangle parameters\n", "s1 = widgets.IntSlider(value=0, min=0, max=2, step=1, description='Weather', continuous_update=False, readout=False)\n", "s2 = widgets.IntSlider(value=0, min=0, max=9, step=1, description='Shut. Speed', continuous_update=False, readout=False)\n", "s3 = widgets.IntSlider(value=0, min=0, max=9, step=1, description='Aperture', continuous_update=False, readout=False)\n", "s4 = widgets.IntSlider(value=0, min=0, max=9, step=1, description='ISO', continuous_update=False, readout=False)\n", "\n", "# Now we link the sliders to the 'on_change' function\n", "s1.observe(on_change, 'value')\n", "s2.observe(on_change, 'value')\n", "s3.observe(on_change, 'value')\n", "s4.observe(on_change, 'value')\n", "\n", "# Let's organise all the widgets in a single UI\n", "sliderBox = VBox()\n", "sliderBox.children = [s1, s2, s3, s4, html]\n", "ui = HBox()\n", "ui.children = [sliderBox, output]\n", "\n", "# Let's initialise the display (HTML and image)\n", "html.value = tri.prettyString() \n", "with output: display(tri.getImage())\n", "\n", "# Show the UI\n", "display(ui)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.6.9" } }, "nbformat": 4, "nbformat_minor": 4 }