diff --git a/exposure_triangle.ipynb b/exposure_triangle.ipynb new file mode 100644 index 0000000..c135e35 --- /dev/null +++ b/exposure_triangle.ipynb @@ -0,0 +1,290 @@ +{ + "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 +} diff --git a/images/background.jpg b/images/background.jpg new file mode 100644 index 0000000..15cabe1 Binary files /dev/null and b/images/background.jpg differ diff --git a/images/flying.png b/images/flying.png new file mode 100644 index 0000000..afc2135 Binary files /dev/null and b/images/flying.png differ